mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-24 13:02:16 +00:00
merge of features from hosted branch (#7)
major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org
This commit is contained in:
+84
-37
@@ -1,56 +1,103 @@
|
||||
const Strategy = require('passport-http-bearer').Strategy;
|
||||
const {getMysqlConnection} = require('../db');
|
||||
const {hashString} = require('../utils/password-utils');
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM api_keys
|
||||
WHERE api_keys.token = ?`;
|
||||
|
||||
function makeStrategy(logger) {
|
||||
function makeStrategy(logger, retrieveKey) {
|
||||
return new Strategy(
|
||||
function(token, done) {
|
||||
logger.info(`validating with token ${token}`);
|
||||
getMysqlConnection((err, conn) => {
|
||||
async function(token, done) {
|
||||
logger.debug(`validating with token ${token}`);
|
||||
jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => {
|
||||
if (err) {
|
||||
logger.error(err, 'Error retrieving mysql connection');
|
||||
return done(err);
|
||||
}
|
||||
conn.query(sql, [token], (err, results, fields) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error(err, 'Error querying for api key');
|
||||
return done(err);
|
||||
}
|
||||
if (0 == results.length) return done(null, false);
|
||||
if (results.length > 1) {
|
||||
logger.info(`api key ${token} exists in multiple rows of api_keys table!!`);
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
logger.debug('jwt expired');
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
// found api key
|
||||
const scope = [];
|
||||
if (results[0].account_sid === null && results[0].service_provider_sid === null) {
|
||||
scope.push.apply(scope, ['admin', 'service_provider', 'account']);
|
||||
/* its not a jwt obtained through login, check api leys */
|
||||
checkApiTokens(logger, token, done);
|
||||
}
|
||||
else {
|
||||
/* validated -- make sure it is not on blacklist */
|
||||
try {
|
||||
const s = `jwt:${hashString(token)}`;
|
||||
const result = await retrieveKey(s);
|
||||
if (result) {
|
||||
debug(`result from searching for ${s}: ${result}`);
|
||||
logger.info('jwt invalidated after logout');
|
||||
return done(null, false);
|
||||
}
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
logger.info({err}, 'Error checking blacklist for jwt');
|
||||
}
|
||||
else if (results[0].service_provider_sid) {
|
||||
scope.push.apply(scope, ['service_provider', 'account']);
|
||||
}
|
||||
else {
|
||||
scope.push('account');
|
||||
}
|
||||
|
||||
const {user_sid, account_sid, email, name} = decoded;
|
||||
//logger.debug({user_sid, account_sid}, 'successfully validated jwt');
|
||||
const scope = ['account'];
|
||||
const user = {
|
||||
account_sid: results[0].account_sid,
|
||||
service_provider_sid: results[0].service_provider_sid,
|
||||
hasScope: (s) => scope.includes(s),
|
||||
hasAdminAuth: scope.length === 3,
|
||||
hasServiceProviderAuth: scope.includes('service_provider') && !scope.includes('admin'),
|
||||
hasAccountAuth: scope.includes('account') && !scope.includes('service_provider')
|
||||
account_sid,
|
||||
user_sid,
|
||||
jwt: token,
|
||||
email,
|
||||
name,
|
||||
hasScope: (s) => s === 'account',
|
||||
hasAdminAuth: false,
|
||||
hasServiceProviderAuth: false,
|
||||
hasAccountAuth: true
|
||||
};
|
||||
logger.info(user, `successfully validated with scope ${scope}`);
|
||||
return done(null, user, {scope});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const checkApiTokens = (logger, token, done) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) {
|
||||
logger.error(err, 'Error retrieving mysql connection');
|
||||
return done(err);
|
||||
}
|
||||
conn.query(sql, [token], (err, results, fields) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error(err, 'Error querying for api key');
|
||||
return done(err);
|
||||
}
|
||||
if (0 == results.length) return done(null, false);
|
||||
if (results.length > 1) {
|
||||
logger.info(`api key ${token} exists in multiple rows of api_keys table!!`);
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
// found api key
|
||||
const scope = [];
|
||||
if (results[0].account_sid === null && results[0].service_provider_sid === null) {
|
||||
scope.push.apply(scope, ['admin', 'service_provider', 'account']);
|
||||
}
|
||||
else if (results[0].service_provider_sid) {
|
||||
scope.push.apply(scope, ['service_provider', 'account']);
|
||||
}
|
||||
else {
|
||||
scope.push('account');
|
||||
}
|
||||
|
||||
const user = {
|
||||
account_sid: results[0].account_sid,
|
||||
service_provider_sid: results[0].service_provider_sid,
|
||||
hasScope: (s) => scope.includes(s),
|
||||
hasAdminAuth: scope.length === 3,
|
||||
hasServiceProviderAuth: scope.includes('service_provider'),
|
||||
hasAccountAuth: scope.includes('account') && !scope.includes('service_provider')
|
||||
};
|
||||
logger.info(user, `successfully validated with scope ${scope}`);
|
||||
return done(null, user, {scope});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = makeStrategy;
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
const getMysqlConnection = require('./mysql');
|
||||
const promisePool = require('./pool');
|
||||
|
||||
module.exports = {
|
||||
getMysqlConnection
|
||||
getMysqlConnection,
|
||||
promisePool
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
const mysql = require('mysql2');
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
@@ -13,6 +13,7 @@ pool.getConnection((err, conn) => {
|
||||
conn.ping((err) => {
|
||||
if (err) return console.error(err, `Error pinging mysql at ${JSON.stringify({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const mysql = require('mysql2');
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
});
|
||||
module.exports = pool.promise();
|
||||
@@ -1,10 +1,57 @@
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const Model = require('./model');
|
||||
const {getMysqlConnection} = require('../db');
|
||||
const {promisePool} = require('../db');
|
||||
const uuid = require('uuid').v4;
|
||||
const {encrypt} = require('../utils/encrypt-decrypt');
|
||||
|
||||
const retrieveSql = `SELECT * from accounts acc
|
||||
LEFT JOIN webhooks AS rh
|
||||
ON acc.registration_hook_sid = rh.webhook_sid`;
|
||||
|
||||
const insertPendingAccountSubscriptionSql = `INSERT account_subscriptions
|
||||
(account_subscription_sid, account_sid, pending, stripe_subscription_id,
|
||||
stripe_payment_method_id, last4, exp_month, exp_year, card_type)
|
||||
VALUES (?,?,1,?,?,?,?,?,?)`;
|
||||
|
||||
const activateSubscriptionSql = `UPDATE account_subscriptions
|
||||
SET pending=0, effective_start_date = CURRENT_TIMESTAMP, stripe_subscription_id = ?
|
||||
WHERE account_subscription_sid = ?
|
||||
AND pending=1`;
|
||||
|
||||
const queryPendingSubscriptionSql = `SELECT * FROM account_subscriptions
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND pending=1`;
|
||||
|
||||
const deactivateSubscriptionSql = `UPDATE account_subscriptions
|
||||
SET pending=1, pending_reason = ?
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND pending=0`;
|
||||
|
||||
const updatePaymentInfoSql = `UPDATE account_subscriptions
|
||||
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL`;
|
||||
|
||||
const insertAccountProductsSql = `INSERT account_products
|
||||
(account_product_sid, account_subscription_sid, product_sid, quantity)
|
||||
VALUES (?,?,?,?);
|
||||
`;
|
||||
|
||||
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
|
||||
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND account_subscription_sid <> ?`;
|
||||
|
||||
const retrieveActiveSubscriptionSql = `SELECT *
|
||||
FROM account_subscriptions
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND pending = 0`;
|
||||
|
||||
function transmogrifyResults(results) {
|
||||
return results.map((row) => {
|
||||
const obj = row.acc;
|
||||
@@ -73,6 +120,111 @@ class Account extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
static async updateStripeCustomerId(sid, customerId) {
|
||||
await promisePool.execute(
|
||||
'UPDATE accounts SET stripe_customer_id = ? WHERE account_sid = ?',
|
||||
[customerId, sid]);
|
||||
}
|
||||
|
||||
static async getSubscription(sid) {
|
||||
const [r] = await promisePool.execute(retrieveActiveSubscriptionSql, [sid]);
|
||||
debug(r, `Account.getSubscription ${sid}`);
|
||||
return r.length > 0 ? r[0] : null;
|
||||
}
|
||||
|
||||
static async deactivateSubscription(logger, account_sid, reason) {
|
||||
logger.debug('deactivateSubscription');
|
||||
|
||||
/**
|
||||
* Two cases:
|
||||
* (1) A subscription renewal fails. In this case we deactivate subscription
|
||||
* and the customer is down until they provide payment.
|
||||
* (2) A customer adds capacity during the month, and the pro-rated amount fails.
|
||||
* In this case, we leave the new subscription in a pending state
|
||||
* The customer continues (for the rest of the month at least) at
|
||||
* previous capacity levels.
|
||||
*/
|
||||
const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid);
|
||||
if (r.length > 0) {
|
||||
/* leave new subscription pending */
|
||||
await promisePool.execute(
|
||||
'UPDATE account_subscriptions set pending_reason = ? WHERE account_subscription_sid = ?',
|
||||
[reason, r[0].account_subscription_sid]);
|
||||
logger.debug('deactivateSubscription - leave pending subscription in pending state');
|
||||
}
|
||||
else {
|
||||
/* deactivate their current active subscription */
|
||||
const [r] = await promisePool.execute(deactivateSubscriptionSql, [reason, account_sid]);
|
||||
logger.debug('deactivateSubscription - deactivated subscription; customer will not have service');
|
||||
return 1 == r.affectedRows;
|
||||
}
|
||||
}
|
||||
|
||||
static async activateSubscription(logger, account_sid, subscription_id, reason) {
|
||||
logger.debug('activateSubscription');
|
||||
|
||||
const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid);
|
||||
if (0 === r.length) return false;
|
||||
|
||||
const [r2] = await promisePool.execute(activateSubscriptionSql,
|
||||
[subscription_id, r[0].account_subscription_sid]);
|
||||
if (0 === r2.affectedRows) return false;
|
||||
|
||||
/* disable the old subscription, if any */
|
||||
const [r3] = await promisePool.execute(replaceOldSubscriptionSql, [
|
||||
reason, account_sid, r[0].account_subscription_sid]);
|
||||
debug(r3, 'Account.activateSubscription - replaced old subscription');
|
||||
|
||||
/* update account.plan to paid, if it isnt already */
|
||||
await promisePool.execute(
|
||||
'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?',
|
||||
[account_sid]);
|
||||
return true;
|
||||
}
|
||||
|
||||
static async updatePaymentInfo(logger, account_sid, pm) {
|
||||
const {card} = pm;
|
||||
const last4_encrypted = encrypt(card.last4);
|
||||
await promisePool.execute(updatePaymentInfoSql,
|
||||
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
|
||||
}
|
||||
|
||||
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
|
||||
logger.debug('provisionPendingSubscription');
|
||||
const account_subscription_sid = uuid();
|
||||
const {id, card} = payment_method;
|
||||
|
||||
/* add a row to account_subscription */
|
||||
let last4_encrypted = null;
|
||||
if (card) {
|
||||
last4_encrypted = encrypt(card.last4);
|
||||
}
|
||||
const [r] = await promisePool.execute(insertPendingAccountSubscriptionSql, [
|
||||
account_subscription_sid,
|
||||
account_sid,
|
||||
subscription_id || null,
|
||||
id,
|
||||
last4_encrypted,
|
||||
card ? card.exp_month : null,
|
||||
card ? card.exp_year : null,
|
||||
card ? card.brand : null
|
||||
]);
|
||||
debug(r, 'Account.activateSubscription - insert account_subscriptions');
|
||||
if (r.affectedRows !== 1) {
|
||||
throw new Error(`failed inserting account_subscriptions for accunt_sid ${account_sid}`);
|
||||
}
|
||||
|
||||
/* add a row for each product to account_products */
|
||||
await Promise.all(products.map((product) => {
|
||||
const {product_sid, quantity} = product;
|
||||
const account_products_sid = uuid();
|
||||
return promisePool.execute(insertAccountProductsSql, [
|
||||
account_products_sid, account_subscription_sid, product_sid, quantity
|
||||
]);
|
||||
}));
|
||||
return account_subscription_sid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Account.table = 'accounts';
|
||||
@@ -103,6 +255,30 @@ Account.fields = [
|
||||
{
|
||||
name: 'device_calling_application_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'plan_type',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'stripe_customer_id',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'webhook_secret',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'disable_cdrs',
|
||||
type: 'number',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
const Model = require('./model');
|
||||
const {getMysqlConnection} = require('../db');
|
||||
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
|
||||
|
||||
class PhoneNumber extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query(sql, account_sid, (err, results, fields) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve an application
|
||||
*/
|
||||
static retrieve(sid, account_sid) {
|
||||
if (!account_sid) return super.retrieve(sid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid], (err, results, fields) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PhoneNumber.table = 'phone_numbers';
|
||||
@@ -20,8 +56,7 @@ PhoneNumber.fields = [
|
||||
},
|
||||
{
|
||||
name: 'voip_carrier_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const Model = require('./model');
|
||||
|
||||
class PredefinedCarrier extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
PredefinedCarrier.table = 'predefined_carriers';
|
||||
PredefinedCarrier.fields = [
|
||||
{
|
||||
name: 'predefined_carrier_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'requires_static_ip',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'e164_leading_plus',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'requires_register',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'register_username',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'register_sip_realm',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'register_password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'tech_prefix',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'inbound_auth_username',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'inbound_auth_password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'diversion',
|
||||
type: 'string'
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = PredefinedCarrier;
|
||||
@@ -0,0 +1,28 @@
|
||||
const Model = require('./model');
|
||||
|
||||
class Product extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
Product.table = 'products';
|
||||
Product.fields = [
|
||||
{
|
||||
name: 'product_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = Product;
|
||||
@@ -1,9 +1,18 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from sip_gateways WHERE voip_carrier_sid = ?';
|
||||
|
||||
class SipGateway extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
/**
|
||||
* list all sip gateways for a voip_carrier
|
||||
*/
|
||||
static async retrieveForVoipCarrier(voip_carrier_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
SipGateway.table = 'sip_gateways';
|
||||
@@ -26,6 +35,10 @@ SipGateway.fields = [
|
||||
name: 'port',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'netmask',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'inbound',
|
||||
type: 'number'
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const Model = require('./model');
|
||||
const {getMysqlConnection} = require('../db');
|
||||
|
||||
class Smpp extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* list all SBCs either for a given service provider, or those not associated with a
|
||||
* service provider (i.e. community SBCs)
|
||||
*/
|
||||
static retrieveAll(service_provider_sid) {
|
||||
const sql = service_provider_sid ?
|
||||
'SELECT * from smpp_addresses WHERE service_provider_sid = ?' :
|
||||
'SELECT * from smpp_addresses WHERE service_provider_sid IS NULL';
|
||||
const args = service_provider_sid ? [service_provider_sid] : [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query(sql, args, (err, results) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Smpp.table = 'smpp_addresses';
|
||||
Smpp.fields = [
|
||||
{
|
||||
name: 'smpp_address_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'ipv4',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'service_provider_sid',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Smpp;
|
||||
@@ -0,0 +1,60 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from smpp_gateways WHERE voip_carrier_sid = ?';
|
||||
|
||||
class SmppGateway extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
/**
|
||||
* list all sip gateways for a voip_carrier
|
||||
*/
|
||||
static async retrieveForVoipCarrier(voip_carrier_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
SmppGateway.table = 'smpp_gateways';
|
||||
SmppGateway.fields = [
|
||||
{
|
||||
name: 'smpp_gateway_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'voip_carrier_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'ipv4',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'netmask',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'inbound',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'outbound',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'is_primary',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'use_tls',
|
||||
type: 'number'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = SmppGateway;
|
||||
@@ -0,0 +1,92 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
|
||||
|
||||
class SpeechCredential extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* list all credentials for an account
|
||||
*/
|
||||
static async retrieveAll(account_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSql, account_sid);
|
||||
return rows;
|
||||
}
|
||||
static async retrieveAllForSP(service_provider_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async disableStt(account_sid) {
|
||||
await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]);
|
||||
}
|
||||
static async disableTts(account_sid) {
|
||||
await promisePool.execute('UPDATE speech_credentials SET use_for_tts = 0 WHERE account_sid = ?', [account_sid]);
|
||||
}
|
||||
|
||||
static async ttsTestResult(sid, success) {
|
||||
await promisePool.execute(
|
||||
'UPDATE speech_credentials SET last_tested = NOW(), tts_tested_ok = ? WHERE speech_credential_sid = ?',
|
||||
[success, sid]);
|
||||
}
|
||||
static async sttTestResult(sid, success) {
|
||||
await promisePool.execute(
|
||||
'UPDATE speech_credentials SET last_tested = NOW(), stt_tested_ok = ? WHERE speech_credential_sid = ?',
|
||||
[success, sid]);
|
||||
}
|
||||
}
|
||||
|
||||
SpeechCredential.table = 'speech_credentials';
|
||||
SpeechCredential.fields = [
|
||||
{
|
||||
name: 'speech_credential_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'service_provider_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'vendor',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'credential',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'use_for_tts',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'use_for_stt',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'tts_tested_ok',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'stt_tested_ok',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'last_used',
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
name: 'last_tested',
|
||||
type: 'date'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = SpeechCredential;
|
||||
@@ -1,9 +1,22 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from voip_carriers vc WHERE vc.account_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from voip_carriers vc WHERE vc.service_provider_sid = ?';
|
||||
|
||||
|
||||
class VoipCarrier extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
static async retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
const [rows] = await promisePool.query(retrieveSql, account_sid);
|
||||
return rows;
|
||||
}
|
||||
static async retrieveAllForSP(service_provider_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
VoipCarrier.table = 'voip_carriers';
|
||||
@@ -26,6 +39,10 @@ VoipCarrier.fields = [
|
||||
name: 'account_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'service_provider_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'application_sid',
|
||||
type: 'string'
|
||||
@@ -49,7 +66,51 @@ VoipCarrier.fields = [
|
||||
{
|
||||
name: 'register_password',
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'tech_prefix',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'inbound_auth_username',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'inbound_auth_password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'diversion',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'smpp_system_id',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'smpp_password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'smpp_inbound_system_id',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'smpp_inbound_password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'smpp_enquire_link_interval',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'smpp_system_id',
|
||||
type: 'string'
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = VoipCarrier;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const router = require('express').Router();
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
const retrieveApplicationsSql = `SELECT * from applications app
|
||||
LEFT JOIN webhooks AS ch
|
||||
ON app.call_hook_sid = ch.webhook_sid
|
||||
LEFT JOIN webhooks AS sh
|
||||
ON app.call_status_hook_sid = sh.webhook_sid
|
||||
LEFT JOIN webhooks AS mh
|
||||
ON app.messaging_hook_sid = mh.webhook_sid
|
||||
WHERE service_provider_sid = ?`;
|
||||
|
||||
const transmogrifyResults = (results) => {
|
||||
return results.map((row) => {
|
||||
const obj = row.app;
|
||||
if (row.ch && Object.keys(row.ch).length && row.ch.url !== null) {
|
||||
Object.assign(obj, {call_hook: row.ch});
|
||||
}
|
||||
else obj.call_hook = null;
|
||||
if (row.sh && Object.keys(row.sh).length && row.sh.url !== null) {
|
||||
Object.assign(obj, {call_status_hook: row.sh});
|
||||
}
|
||||
else obj.call_status_hook = null;
|
||||
if (row.mh && Object.keys(row.mh).length && row.mh.url !== null) {
|
||||
Object.assign(obj, {messaging_hook: row.mh});
|
||||
}
|
||||
else obj.messaging_hook = null;
|
||||
delete obj.call_hook_sid;
|
||||
delete obj.call_status_hook_sid;
|
||||
delete obj.messaging_hook_sid;
|
||||
return obj;
|
||||
});
|
||||
};
|
||||
|
||||
router.get('/:service_provider_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {service_provider_sid} = req.params;
|
||||
|
||||
try {
|
||||
const [r] = await promisePool.query('SELECT * from service_providers where service_provider_sid = ?',
|
||||
service_provider_sid);
|
||||
if (r.length === 0) {
|
||||
logger.info(`/AccountTest invalid service_provider_sid ${service_provider_sid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
const [numbers] = await promisePool.query('SELECT number FROM phone_numbers WHERE service_provider_sid = ?',
|
||||
service_provider_sid);
|
||||
const [results] = await promisePool.query({sql: retrieveApplicationsSql, nestTables: true}, service_provider_sid);
|
||||
|
||||
res.json({phonenumbers: numbers.map((n) => n.number), applications: transmogrifyResults(results)});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+154
-22
@@ -2,20 +2,65 @@ const router = require('express').Router();
|
||||
const request = require('request');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
const Application = require('../../models/application');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const ApiKey = require('../../models/api-key');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
const {deleteDnsRecords} = require('../../utils/dns-utils');
|
||||
const {deleteCustomer} = require('../../utils/stripe-utils');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const decorate = require('./decorate');
|
||||
const snakeCase = require('../../utils/snake-case');
|
||||
const sysError = require('./error');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate,
|
||||
'delete': validateDelete
|
||||
};
|
||||
const sysError = require('../error');
|
||||
const {promisePool} = require('../../db');
|
||||
const {hasAccountPermissions, parseAccountSid} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const translator = short();
|
||||
|
||||
let idx = 0;
|
||||
|
||||
router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials'));
|
||||
router.use('/:sid/RecentCalls', hasAccountPermissions, require('./recent-calls'));
|
||||
router.use('/:sid/Alerts', hasAccountPermissions, require('./alerts'));
|
||||
router.use('/:sid/Charges', hasAccountPermissions, require('./charges'));
|
||||
router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm'));
|
||||
router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier'));
|
||||
router.get('/:sid/Applications', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const results = await Application.retrieveAll(null, account_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const results = await VoipCarrier.retrieveAll(account_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const payload = req.body;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
logger.debug({payload}, 'POST /:sid/VoipCarriers');
|
||||
const uuid = await VoipCarrier.make({
|
||||
account_sid,
|
||||
...payload
|
||||
});
|
||||
res.status(201).json({sid: uuid});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
function coerceNumbers(callInfo) {
|
||||
if (Array.isArray(callInfo)) {
|
||||
return callInfo.map((ci) => {
|
||||
@@ -59,7 +104,7 @@ function validateUpdateCall(opts) {
|
||||
break;
|
||||
case 2:
|
||||
if (opts.call_hook && opts.child_call_hook) break;
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default:
|
||||
throw new DbErrorBadRequest('multiple options are not allowed in updateCall');
|
||||
}
|
||||
@@ -167,13 +212,14 @@ async function validateCreateCall(logger, sid, req) {
|
||||
|
||||
async function validateCreateMessage(logger, sid, req) {
|
||||
const obj = req.body;
|
||||
const {lookupAccountByPhoneNumber} = req.app.locals;
|
||||
//const {lookupAccountByPhoneNumber} = req.app.locals;
|
||||
|
||||
if (req.user.account_sid !== sid) {
|
||||
throw new DbErrorBadRequest(`unauthorized createMessage request for account ${sid}`);
|
||||
}
|
||||
|
||||
if (!obj.from) throw new DbErrorBadRequest('missing from property');
|
||||
/*
|
||||
else {
|
||||
const regex = /^\+(\d+)$/;
|
||||
const arr = regex.exec(obj.from);
|
||||
@@ -181,7 +227,7 @@ async function validateCreateMessage(logger, sid, req) {
|
||||
const account = await lookupAccountByPhoneNumber(from);
|
||||
if (!account) throw new DbErrorBadRequest(`accountSid ${sid} does not own phone number ${from}`);
|
||||
}
|
||||
|
||||
*/
|
||||
if (!obj.to) throw new DbErrorBadRequest('missing to property');
|
||||
|
||||
if (!obj.text && !obj.media) {
|
||||
@@ -194,7 +240,7 @@ async function validateAdd(req) {
|
||||
if (req.user.hasAccountAuth) {
|
||||
throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts');
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) {
|
||||
/* service providers can only create accounts under themselves */
|
||||
req.body.service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
@@ -212,6 +258,9 @@ async function validateUpdate(req, sid) {
|
||||
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
|
||||
}
|
||||
if (req.user.hasAccountAuth && req.body.sip_realm) {
|
||||
throw new DbErrorBadRequest('use POST /Accounts/:sid/sip_realm/:realm to set or change the sip realm');
|
||||
}
|
||||
|
||||
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
|
||||
const result = await Account.retrieve(sid);
|
||||
@@ -225,8 +274,6 @@ async function validateDelete(req, sid) {
|
||||
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
|
||||
}
|
||||
const assignedPhoneNumbers = await Account.getForeignKeyReferences('phone_numbers.account_sid', sid);
|
||||
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete account with phone numbers');
|
||||
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
|
||||
const result = await Account.retrieve(sid);
|
||||
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
@@ -235,16 +282,16 @@ async function validateDelete(req, sid) {
|
||||
}
|
||||
}
|
||||
|
||||
decorate(router, Account, ['delete'], preconditions);
|
||||
|
||||
/* add */
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const secret = `wh_secret_${translator.generate()}`;
|
||||
await validateAdd(req);
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
const obj = Object.assign({webhook_secret: secret}, req.body);
|
||||
for (const prop of ['registration_hook']) {
|
||||
if (obj[prop]) {
|
||||
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
|
||||
@@ -252,7 +299,7 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
//logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
|
||||
logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
|
||||
const uuid = await Account.make(obj);
|
||||
res.status(201).json({sid: uuid});
|
||||
} catch (err) {
|
||||
@@ -287,6 +334,25 @@ router.get('/:sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/WebhookSecret', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const results = await Account.retrieve(req.params.sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
let {webhook_secret} = results[0];
|
||||
if (req.query.regenerate) {
|
||||
const secret = `wh_secret_${translator.generate()}`;
|
||||
await Account.update(req.params.sid, {webhook_secret: secret});
|
||||
webhook_secret = secret;
|
||||
}
|
||||
return res.status(200).json({webhook_secret});
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* update */
|
||||
router.put('/:sid', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
@@ -342,6 +408,68 @@ router.put('/:sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* delete */
|
||||
router.delete('/:sid', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const logger = req.app.locals.logger;
|
||||
const sqlDeleteGateways = `DELETE from sip_gateways
|
||||
WHERE voip_carrier_sid IN
|
||||
(SELECT voip_carrier_sid from voip_carriers where account_sid = ?)`;
|
||||
try {
|
||||
await validateDelete(req, sid);
|
||||
|
||||
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
|
||||
const {sip_realm, stripe_customer_id} = account[0];
|
||||
/* remove dns records */
|
||||
if (process.env.NODE_ENV !== 'test' || process.env.DME_API_KEY) {
|
||||
|
||||
/* retrieve existing dns records */
|
||||
const [recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?', sid);
|
||||
|
||||
if (recs.length > 0) {
|
||||
/* remove existing records from the database and dns provider */
|
||||
const arr = /(.*)\.(.*\..*)$/.exec(sip_realm);
|
||||
if (!arr) throw new DbErrorBadRequest(`invalid sip_realm: ${sip_realm}`);
|
||||
const domain = arr[2];
|
||||
|
||||
await promisePool.query('DELETE from dns_records WHERE account_sid = ?', sid);
|
||||
const deleted = await deleteDnsRecords(logger, domain, recs.map((r) => r.record_id));
|
||||
if (!deleted) {
|
||||
logger.error({recs, sip_realm, sid},
|
||||
'Failed to remove old dns records when changing sip_realm for account');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await promisePool.execute('DELETE from api_keys where account_sid = ?', [sid]);
|
||||
await promisePool.execute(
|
||||
// eslint-disable-next-line indent
|
||||
`DELETE from account_products
|
||||
WHERE account_subscription_sid IN
|
||||
(SELECT account_subscription_sid FROM
|
||||
account_subscriptions WHERE account_sid = ?)
|
||||
`, [sid]);
|
||||
await promisePool.execute('DELETE from account_subscriptions WHERE account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from speech_credentials where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from users where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from phone_numbers where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from call_routes where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from ms_teams_tenants where account_sid = ?', [sid]);
|
||||
await promisePool.execute(sqlDeleteGateways, [sid]);
|
||||
await promisePool.execute('DELETE from voip_carriers where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from applications where account_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE from accounts where account_sid = ?', [sid]);
|
||||
|
||||
if (stripe_customer_id) {
|
||||
const response = await deleteCustomer(logger, stripe_customer_id);
|
||||
logger.info({response}, `deleted stripe customer_id ${stripe_customer_id} for account_si ${sid}`);
|
||||
}
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* retrieve account level api keys */
|
||||
router.get('/:sid/ApiKeys', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
@@ -504,7 +632,7 @@ router.put('/:sid/Calls/:callSid', async(req, res) => {
|
||||
* create a new Message
|
||||
*/
|
||||
router.post('/:sid/Messages', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const account_sid = parseAccountSid(req);
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
const {retrieveSet, logger} = req.app.locals;
|
||||
|
||||
@@ -516,12 +644,16 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
}
|
||||
const ip = fs[idx++ % fs.length];
|
||||
logger.info({fs}, `feature servers available for createMessage API request, selecting ${ip}`);
|
||||
const serviceUrl = `http://${ip}:3000/v1/createMessage/${sid}`;
|
||||
await validateCreateMessage(logger, sid, req);
|
||||
const serviceUrl = `http://${ip}:3000/v1/createMessage/${account_sid}`;
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
|
||||
const payload = Object.assign({messageSid: uuidv4(), account_sid: sid}, req.body);
|
||||
const payload = {
|
||||
message_sid: uuidv4(),
|
||||
account_sid,
|
||||
...req.body
|
||||
};
|
||||
logger.debug({payload}, `sending createMessage API request to to ${ip}`);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
updateLastUsed(logger, account_sid, req).catch(() => {});
|
||||
request({
|
||||
url: serviceUrl,
|
||||
method: 'POST',
|
||||
@@ -534,7 +666,7 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
return res.sendStatus(response.statusCode);
|
||||
}
|
||||
res.status(201).json(body);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
const router = require('express').Router();
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
|
||||
const sysError = require('../error');
|
||||
const sqlRetrieveUser = `SELECT * from users user
|
||||
LEFT JOIN accounts AS account
|
||||
ON user.account_sid = account.account_sid
|
||||
WHERE user.user_sid = ?`;
|
||||
|
||||
const validateRequest = async(req, res) => {
|
||||
const payload = req.body || {};
|
||||
|
||||
/* valid type */
|
||||
if (!['email', 'phone'].includes(payload.type)) {
|
||||
throw new DbErrorBadRequest(`invalid activation type: ${payload.type}`);
|
||||
}
|
||||
|
||||
/* valid user? */
|
||||
const [rows] = await promisePool.query('SELECT * from users WHERE user_sid = ?',
|
||||
payload.user_sid);
|
||||
if (0 === rows.length) throw new DbErrorBadRequest('invalid user_sid');
|
||||
|
||||
/* valid email? */
|
||||
if (payload.type === 'email' && !validateEmail(payload.value)) throw new DbErrorBadRequest('invalid email');
|
||||
|
||||
};
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid, type, code, value} = req.body;
|
||||
|
||||
try {
|
||||
await validateRequest(req, res);
|
||||
|
||||
const fields = type === 'email' ?
|
||||
['email', 'email_validated', 'email_activation_code'] :
|
||||
['phone', 'phone_validated', 'phone_activation_code'];
|
||||
const sql =
|
||||
`UPDATE users set ${fields[0]} = ?, ${fields[1]} = 0, ${fields[2]} = ? WHERE user_sid = ?`;
|
||||
const [r] = await promisePool.execute(sql, [value, code, user_sid]);
|
||||
logger.debug({r}, 'Result from adding activation code');
|
||||
debug({r}, 'Result from adding activation code');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (type === 'email') {
|
||||
/* send code via email */
|
||||
const text = '';
|
||||
const subject = '';
|
||||
await emailSimpleText(logger, value, subject, text);
|
||||
}
|
||||
else {
|
||||
/* send code via SMS */
|
||||
}
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:code', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const code = req.params.code;
|
||||
const {user_sid, type} = req.body;
|
||||
|
||||
try {
|
||||
let activateAccount = false;
|
||||
let deactivateOldUsers = false;
|
||||
let account_sid;
|
||||
if (type === 'email') {
|
||||
/* check whether this is first-time activation of account during sign-up/register */
|
||||
const [r] = await promisePool.query({sql: sqlRetrieveUser, nestTables: true}, user_sid);
|
||||
logger.debug({r}, 'activationcode - selected user');
|
||||
if (r.length) {
|
||||
const {user, account} = r[0];
|
||||
account_sid = account.account_sid;
|
||||
const [otherUsers] = await promisePool.query('SELECT * from users WHERE account_sid = ? AND user_sid <> ?',
|
||||
[account_sid, user_sid]);
|
||||
logger.debug({otherUsers}, `activationcode - users other than ${user_sid}`);
|
||||
if (0 === otherUsers.length && user.provider === 'local' && !user.email_validated) {
|
||||
logger.debug('activationcode - activating account');
|
||||
activateAccount = true;
|
||||
}
|
||||
else if (otherUsers.length) {
|
||||
logger.debug('activationcode - adding new user for existing account');
|
||||
deactivateOldUsers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const fields = type === 'email' ?
|
||||
['email_validated', 'email_activation_code'] :
|
||||
['phone_validated', 'phone_activation_code'];
|
||||
const sql = `UPDATE users set ${fields[0]} = 1, ${fields[1]} = NULL WHERE ${fields[1]} = ? AND user_sid = ?`;
|
||||
const [r] = await promisePool.execute(sql, [code, user_sid]);
|
||||
logger.debug({r}, 'Result from validating code');
|
||||
debug({r}, 'Result from validating code');
|
||||
|
||||
if (activateAccount) {
|
||||
await promisePool.execute('UPDATE accounts SET is_active=1 WHERE account_sid = ?', [account_sid]);
|
||||
}
|
||||
else if (deactivateOldUsers) {
|
||||
const [r] = await promisePool.execute('DELETE FROM users WHERE account_sid = ? AND user_sid <> ?',
|
||||
[account_sid, user_sid]);
|
||||
logger.debug({r}, 'Result from deleting old/replaced users');
|
||||
}
|
||||
|
||||
if (1 === r.affectedRows) return res.sendStatus(204);
|
||||
throw new DbErrorBadRequest('invalid user or activation code');
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,71 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const PredefinedCarrier = require('../../models/predefined-carrier');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const SipGateway = require('../../models/sip-gateway');
|
||||
const {parseServiceProviderSid} = require('./utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
|
||||
const sqlSelectCarrierByName = `SELECT * FROM voip_carriers
|
||||
WHERE account_sid = ?
|
||||
AND name = ?`;
|
||||
const sqlSelectCarrierByNameForSP = `SELECT * FROM voip_carriers
|
||||
WHERE service_provider_sid = ?
|
||||
AND name = ?`;
|
||||
const sqlSelectTemplateGateways = `SELECT * FROM predefined_sip_gateways
|
||||
WHERE predefined_carrier_sid = ?`;
|
||||
|
||||
|
||||
router.post('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {sid } = req.params;
|
||||
let service_provider_sid;
|
||||
const {account_sid} = req.user;
|
||||
if (!account_sid) {
|
||||
if (!req.user.hasScope('service_provider')) {
|
||||
logger.error({user: req.user}, 'invalid creds');
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
service_provider_sid = parseServiceProviderSid(req);
|
||||
}
|
||||
try {
|
||||
const [template] = await PredefinedCarrier.retrieve(sid);
|
||||
logger.debug({template}, `Retrieved template carrier for sid ${sid}`);
|
||||
if (!template) return res.sendStatus(404);
|
||||
|
||||
/* make sure not to add the same carrier twice */
|
||||
const [r2] = account_sid ?
|
||||
await promisePool.query(sqlSelectCarrierByName, [account_sid, template.name]) :
|
||||
await promisePool.query(sqlSelectCarrierByNameForSP, [service_provider_sid, template.name]);
|
||||
|
||||
if (r2.length > 0) {
|
||||
logger.info({account_sid}, `Failed to add carrier with name ${template.name}, carrier of that name exists`);
|
||||
throw new DbErrorBadRequest(`A carrier with name ${template.name} already exists`);
|
||||
}
|
||||
|
||||
/* retrieve all the gateways */
|
||||
const [r3] = await promisePool.query(sqlSelectTemplateGateways, template.predefined_carrier_sid);
|
||||
logger.debug({r3}, `retrieved template gateways for ${template.name}`);
|
||||
|
||||
/* add a voip_carrier */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {requires_static_ip, predefined_carrier_sid, ...obj} = template;
|
||||
const uuid = await VoipCarrier.make({...obj, account_sid, service_provider_sid});
|
||||
|
||||
/* add all the gateways */
|
||||
for (const gw of r3) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {predefined_carrier_sid, predefined_sip_gateway_sid, ...obj} = gw;
|
||||
logger.debug({obj}, 'adding gateway');
|
||||
await SipGateway.make({...obj, voip_carrier_sid: uuid});
|
||||
}
|
||||
logger.debug({sid: uuid}, 'Successfully added carrier from predefined list');
|
||||
res.status(201).json({sid: uuid});
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error adding voip_carrier from template');
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,35 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const {logger, queryAlerts} = req.app.locals;
|
||||
try {
|
||||
logger.debug({opts: req.query}, 'GET /Alerts');
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const {page, count, alert_type, days, start, end} = req.query || {};
|
||||
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
|
||||
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
const data = await queryAlerts({
|
||||
account_sid,
|
||||
page,
|
||||
page_size: count,
|
||||
alert_type,
|
||||
days,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
});
|
||||
|
||||
res.status(200).json(data);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,7 +5,7 @@ const Account = require('../../models/account');
|
||||
const decorate = require('./decorate');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const assert = require('assert');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
const preconditions = {
|
||||
'add': validateAddToken,
|
||||
'delete': validateDeleteToken
|
||||
|
||||
@@ -4,7 +4,7 @@ const Application = require('../../models/application');
|
||||
const Account = require('../../models/account');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate,
|
||||
@@ -33,8 +33,11 @@ async function validateAdd(req) {
|
||||
}
|
||||
|
||||
async function validateUpdate(req, sid) {
|
||||
if (req.user.account_sid && sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
|
||||
if (req.user.account_sid) {
|
||||
const app = await Application.retrieve(sid);
|
||||
if (!app || !app.length || app[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
|
||||
}
|
||||
}
|
||||
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application');
|
||||
@@ -45,8 +48,12 @@ async function validateUpdate(req, sid) {
|
||||
}
|
||||
|
||||
async function validateDelete(req, sid) {
|
||||
if (req.user.account_sid && sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
|
||||
if (req.user.hasAccountAuth) {
|
||||
const result = await Application.retrieve(sid);
|
||||
if (!result || 0 === result.length) throw new DbErrorBadRequest('application does not exist');
|
||||
if (result[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account');
|
||||
}
|
||||
}
|
||||
const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid);
|
||||
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {type, value} = req.query;
|
||||
|
||||
try {
|
||||
|
||||
if (['email', 'phone'].includes(type)) {
|
||||
const field = type === 'email' ? 'email' : 'phone';
|
||||
const sql = `SELECT * from users WHERE ${field} = ?`;
|
||||
const [r] = await promisePool.execute(sql, [value]);
|
||||
res.json({available: 0 === r.length});
|
||||
}
|
||||
else if (type === 'subdomain') {
|
||||
const sql = 'SELECT * from accounts WHERE sip_realm = ?';
|
||||
const [r] = await promisePool.execute(sql, [value]);
|
||||
res.json({available: 0 === r.length});
|
||||
}
|
||||
else throw new DbErrorBadRequest(`invalid type: ${type}`);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,32 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {promisePool} = require('../../db');
|
||||
const short = require('short-uuid');
|
||||
const translator = short('0123456789ABCXZ');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
logger.debug({payload: req.body}, 'POST /BetaInviteCodes');
|
||||
try {
|
||||
const {count} = req.body || {};
|
||||
const total = Math.max(count || 1, 1);
|
||||
const codes = [];
|
||||
let added = 0;
|
||||
while (added < total) {
|
||||
const code = translator.new().substring(0, 6);
|
||||
if (!codes.find((c) => c === code)) {
|
||||
codes.push(code);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
const values = codes.map((c) => `('${c}')`).join(',');
|
||||
const sql = `INSERT INTO beta_invite_codes (invite_code) VALUES ${values}`;
|
||||
const [r] = await promisePool.query(sql);
|
||||
res.status(200).json({status: 'ok', added: r.affectedRows, codes});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,47 @@
|
||||
const router = require('express').Router();
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
const sqlUpdatePassword = `UPDATE users
|
||||
SET hashed_password= ?
|
||||
WHERE user_sid = ?`;
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, retrieveKey, deleteKey} = req.app.locals;
|
||||
const {user_sid} = req.user;
|
||||
const {old_password, new_password} = req.body;
|
||||
try {
|
||||
if (!old_password || !new_password) throw new DbErrorBadRequest('missing old_password or new_password');
|
||||
|
||||
/* validate existing password */
|
||||
{
|
||||
const [r] = await promisePool.query('SELECT * from users where user_sid = ?', user_sid);
|
||||
logger.debug({user: [r[0]]}, 'change password for user');
|
||||
|
||||
if (r[0].provider !== 'local') {
|
||||
throw new DbErrorBadRequest('user is using oauth authentication');
|
||||
}
|
||||
|
||||
const isCorrect = await verifyPassword(r[0].hashed_password, old_password);
|
||||
if (!isCorrect) {
|
||||
const key = `reset-link:${old_password}`;
|
||||
const user_sid = await retrieveKey(key);
|
||||
if (!user_sid) throw new DbErrorBadRequest('old_password is incorrect');
|
||||
await deleteKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
/* store new password */
|
||||
const passwordHash = await generateHashedPassword(new_password);
|
||||
const [r] = await promisePool.execute(sqlUpdatePassword, [passwordHash, user_sid]);
|
||||
if (r.affectedRows !== 1) throw new Error('failed to update user with new password');
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,46 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
//const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
|
||||
|
||||
|
||||
/**
|
||||
* retrieve charges for an account and/or call
|
||||
*/
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
res.status(200).json([
|
||||
{
|
||||
charge_sid: 'f8d2a604-ed29-4eac-9efc-8f58b0e438ca',
|
||||
account_sid: req.user.account_sid,
|
||||
call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1',
|
||||
billed_at: '2020-01-01 15:10:10',
|
||||
billed_activity: 'outbound-call',
|
||||
call_secs_billed: 392,
|
||||
amount_charged: 0.0200
|
||||
},
|
||||
{
|
||||
charge_sid: 'd9659f3f-3a94-455c-9e8e-3b36f250ffc8',
|
||||
account_sid: req.user.account_sid,
|
||||
call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1',
|
||||
billed_at: '2020-01-01 15:10:10',
|
||||
billed_activity: 'tts',
|
||||
tts_chars_billed: 100,
|
||||
amount_charged: 0.0130
|
||||
},
|
||||
{
|
||||
charge_sid: 'adcc1e79-eb79-4370-ab74-4c2e9a41339a',
|
||||
account_sid: req.user.account_sid,
|
||||
call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1',
|
||||
billed_at: '2020-01-01 15:10:10',
|
||||
billed_activity: 'stt',
|
||||
stt_secs_billed: 30,
|
||||
amount_charged: 0.0015
|
||||
}
|
||||
]);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,5 +1,5 @@
|
||||
const assert = require('assert');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
|
||||
module.exports = decorate;
|
||||
|
||||
@@ -58,7 +58,7 @@ function retrieve(router, klass) {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await klass.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
@@ -78,7 +78,7 @@ function update(router, klass, preconditions) {
|
||||
}
|
||||
const rowsAffected = await klass.update(sid, req.body);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
@@ -99,9 +99,9 @@ function remove(router, klass, preconditions) {
|
||||
const rowsAffected = await klass.remove(sid);
|
||||
if (rowsAffected === 0) {
|
||||
logger.info(`unable to delete ${klass.name} with sid ${sid}: not found`);
|
||||
return res.status(404).end();
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(204).end();
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const router = require('express').Router();
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
const short = require('short-uuid');
|
||||
const translator = short();
|
||||
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
const sql = `SELECT * from users user
|
||||
LEFT JOIN accounts AS acc
|
||||
ON acc.account_sid = user.account_sid
|
||||
WHERE user.email = ?`;
|
||||
|
||||
function createOauthEmailText(provider) {
|
||||
return `Hi there!
|
||||
|
||||
Someone (presumably you!) requested to reset their password.
|
||||
However, the account associated with this email is using oauth identification via ${provider},
|
||||
Please change your password through that provider, if you wish to.
|
||||
|
||||
If you did not make this request, please delete this email. No further action is required.
|
||||
|
||||
Best,
|
||||
|
||||
Jambonz support team`;
|
||||
}
|
||||
|
||||
function createResetEmailText(link) {
|
||||
const baseUrl = 'http://localhost:3001';
|
||||
|
||||
return `Hi there!
|
||||
|
||||
Someone (presumably you!) requested to reset their password.
|
||||
Please follow the link below to reset your password:
|
||||
|
||||
${baseUrl}/reset-password/${link}
|
||||
|
||||
This link is valid for 1 hour only.
|
||||
If you did not make this request, please delete this email. No further action is required.
|
||||
|
||||
Best,
|
||||
|
||||
Jambonz support team`;
|
||||
}
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, addKey} = req.app.locals;
|
||||
const {email} = req.body;
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
return res.status(400).json({error: 'invalid or missing email'});
|
||||
}
|
||||
|
||||
const [r] = await promisePool.query({sql, nestTables: true}, email);
|
||||
if (0 === r.length) {
|
||||
return res.status(400).json({error: 'email does not exist'});
|
||||
}
|
||||
obj = r[0];
|
||||
if (!obj.acc.is_active) {
|
||||
return res.status(400).json({error: 'you may not reset the password of an inactive account'});
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (obj.user.provider !== 'local') {
|
||||
/* send email indicating they need to change via their oauth provider */
|
||||
emailSimpleText(logger, email, 'Reset password request', createOauthEmailText(obj.user.provider));
|
||||
|
||||
}
|
||||
else {
|
||||
/* generate a link for this user to reset, send email */
|
||||
const link = translator.generate();
|
||||
addKey(`reset-link:${link}`, obj.user.user_sid, 3600)
|
||||
.catch((err) => logger.error({err}, 'Error adding reset link to redis'));
|
||||
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+25
-6
@@ -1,26 +1,45 @@
|
||||
const api = require('express').Router();
|
||||
|
||||
function isAdminScope(req, res, next) {
|
||||
const isAdminScope = (req, res, next) => {
|
||||
if (req.user.hasScope('admin')) return next();
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
|
||||
api.use('/ServiceProviders', isAdminScope, require('./service-providers'));
|
||||
api.use('/VoipCarriers', isAdminScope, require('./voip-carriers'));
|
||||
api.use('/SipGateways', isAdminScope, require('./sip-gateways'));
|
||||
api.use('/PhoneNumbers', isAdminScope, require('./phone-numbers'));
|
||||
api.use('/VoipCarriers', require('./voip-carriers'));
|
||||
api.use('/Webhooks', require('./webhooks'));
|
||||
api.use('/SipGateways', require('./sip-gateways'));
|
||||
api.use('/SmppGateways', require('./smpp-gateways'));
|
||||
api.use('/PhoneNumbers', require('./phone-numbers'));
|
||||
api.use('/ApiKeys', require('./api-keys'));
|
||||
api.use('/Accounts', require('./accounts'));
|
||||
api.use('/Applications', require('./applications'));
|
||||
api.use('/MicrosoftTeamsTenants', require('./tenants'));
|
||||
api.use('/Sbcs', isAdminScope, require('./sbcs'));
|
||||
api.use('/Sbcs', require('./sbcs'));
|
||||
api.use('/Users', require('./users'));
|
||||
api.use('/register', require('./register'));
|
||||
api.use('/signin', require('./signin'));
|
||||
api.use('/login', require('./login'));
|
||||
api.use('/logout', require('./logout'));
|
||||
api.use('/forgot-password', require('./forgot-password'));
|
||||
api.use('/change-password', require('./change-password'));
|
||||
api.use('/ActivationCode', require('./activation-code'));
|
||||
api.use('/Availability', require('./availability'));
|
||||
api.use('/AccountTest', require('./account-test'));
|
||||
//api.use('/Products', require('./products'));
|
||||
api.use('/Prices', require('./prices'));
|
||||
api.use('/StripeCustomerId', require('./stripe-customer-id'));
|
||||
api.use('/Subscriptions', require('./subscriptions'));
|
||||
api.use('/Invoices', require('./invoices'));
|
||||
api.use('/InviteCodes', require('./invite-codes'));
|
||||
api.use('/PredefinedCarriers', require('./predefined-carriers'));
|
||||
|
||||
// messaging
|
||||
api.use('/Smpps', require('./smpps')); // our smpp server info
|
||||
api.use('/messaging', require('./sms-inbound')); // inbound SMS from carrier
|
||||
api.use('/outboundSMS', require('./sms-outbound')); // outbound SMS from feature server
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {promisePool} = require('../../db');
|
||||
const sqlClaim = `UPDATE beta_invite_codes
|
||||
SET in_use = 1
|
||||
WHERE invite_code = ?
|
||||
AND in_use = 0`;
|
||||
const sqlTest = `SELECT * FROM beta_invite_codes
|
||||
WHERE invite_code = ?
|
||||
AND in_use = 0`;
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
try {
|
||||
const {code, test} = req.body;
|
||||
logger.debug({code}, 'POST /InviteCodes');
|
||||
if ('test' === process.env.NODE_ENV) {
|
||||
if (code.endsWith('0')) return res.sendStatus(404);
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
if (test) {
|
||||
const [r] = await promisePool.execute(sqlTest, [code]);
|
||||
res.sendStatus(1 === r.length ? 204 : 404);
|
||||
}
|
||||
else {
|
||||
const [r] = await promisePool.execute(sqlClaim, [code]);
|
||||
res.sendStatus(1 === r.affectedRows ? 204 : 404);
|
||||
}
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,26 @@
|
||||
const router = require('express').Router();
|
||||
const assert = require('assert');
|
||||
const Account = require('../../models/account');
|
||||
const {
|
||||
retrieveUpcomingInvoice
|
||||
} = require('../../utils/stripe-utils');
|
||||
const sysError = require('../error');
|
||||
|
||||
/* retrieve */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
try {
|
||||
const results = await Account.retrieve(account_sid);
|
||||
assert.ok(1 === results.length, `account ${account_sid} not found`);
|
||||
const {stripe_customer_id} = results[0];
|
||||
if (!stripe_customer_id) return res.sendStatus(404);
|
||||
const invoice = await retrieveUpcomingInvoice(logger, stripe_customer_id);
|
||||
res.status(200).json(invoice);
|
||||
} catch (err) {
|
||||
if (err.statusCode) return res.sendStatus(err.statusCode);
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+6
-19
@@ -1,19 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
const crypto = require('crypto');
|
||||
const {getMysqlConnection} = require('../../db');
|
||||
const {verifyPassword} = require('../../utils/password-utils');
|
||||
|
||||
const retrieveSql = 'SELECT * from users where name = ?';
|
||||
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest('hex');
|
||||
return {
|
||||
salt:salt,
|
||||
passwordHash:value
|
||||
};
|
||||
};
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
@@ -28,7 +19,7 @@ router.post('/', (req, res) => {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
conn.query(retrieveSql, [username], (err, results) => {
|
||||
conn.query(retrieveSql, [username], async(err, results) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
@@ -40,14 +31,10 @@ router.post('/', (req, res) => {
|
||||
}
|
||||
|
||||
logger.info({results}, 'successfully retrieved account');
|
||||
const salt = results[0].salt;
|
||||
const trueHash = results[0].hashed_password;
|
||||
const forceChange = results[0].force_change;
|
||||
const isCorrect = await verifyPassword(results[0].hashed_password, password);
|
||||
if (!isCorrect) return res.sendStatus(403);
|
||||
|
||||
const {passwordHash} = sha512(password, salt);
|
||||
if (trueHash !== passwordHash) return res.sendStatus(403);
|
||||
|
||||
if (forceChange) return res.json({user_sid: results[0].user_sid, force_change: true});
|
||||
const force_change = !!results[0].force_change;
|
||||
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) {
|
||||
@@ -64,7 +51,7 @@ router.post('/', (req, res) => {
|
||||
logger.error('Database has no admin token provisioned...run reset_admin_password');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
res.json({user_sid: results[0].user_sid, token: tokenResults[0].token});
|
||||
res.json({user_sid: results[0].user_sid, force_change, token: tokenResults[0].token});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const router = require('express').Router();
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const {hashString} = require('../../utils/password-utils');
|
||||
const sysError = require('../error');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, addKey} = req.app.locals;
|
||||
const {jwt} = req.user;
|
||||
|
||||
debug(`adding jwt to blacklist: ${jwt}`);
|
||||
|
||||
try {
|
||||
/* add key to blacklist */
|
||||
const s = `jwt:${hashString(jwt)}`;
|
||||
const result = await addKey(s, '1', 3600);
|
||||
debug(`result from adding ${s}: ${result}`);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,13 +3,14 @@ const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/er
|
||||
const PhoneNumber = require('../../models/phone-number');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const decorate = require('./decorate');
|
||||
const validateNumber = require('../../utils/phone-number-syntax');
|
||||
const {e164} = require('../../utils/phone-number-utils');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'delete': checkInUse,
|
||||
'update': validateUpdate
|
||||
};
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
|
||||
|
||||
/* check for required fields when adding */
|
||||
async function validateAdd(req) {
|
||||
@@ -19,17 +20,19 @@ async function validateAdd(req) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
}
|
||||
|
||||
if (!req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid is required');
|
||||
if (!req.body.number) throw new DbErrorBadRequest('number is required');
|
||||
validateNumber(req.body.number);
|
||||
const formattedNumber = e164(req.body.number);
|
||||
req.body.number = formattedNumber;
|
||||
} catch (err) {
|
||||
throw new DbErrorBadRequest(err.message);
|
||||
}
|
||||
|
||||
/* check that voip carrier exists */
|
||||
const result = await VoipCarrier.retrieve(req.body.voip_carrier_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`);
|
||||
if (req.body.voip_carrier_sid) {
|
||||
const result = await VoipCarrier.retrieve(req.body.voip_carrier_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +40,7 @@ async function validateAdd(req) {
|
||||
async function checkInUse(req, sid) {
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber.account_sid !== req.user.account_sid) {
|
||||
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
|
||||
}
|
||||
}
|
||||
@@ -53,10 +56,16 @@ async function validateUpdate(req, sid) {
|
||||
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
|
||||
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account');
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: if we are assigning to an account, verify it exists
|
||||
|
||||
// TODO: if we are assigning to an application, verify it is associated to the same account
|
||||
|
||||
// TODO: if we are removing from an account, verify we are also removing from application.
|
||||
}
|
||||
|
||||
decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
|
||||
@@ -86,5 +95,4 @@ router.get('/:sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const PredefinedCarrier = require('../../models/predefined-carrier');
|
||||
const decorate = require('./decorate');
|
||||
|
||||
decorate(router, PredefinedCarrier, ['list']);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,108 @@
|
||||
const router = require('express').Router();
|
||||
const Product = require('../../models/product');
|
||||
const {promisePool} = require('../../db');
|
||||
const sysError = require('../error');
|
||||
const sqlRetrieveSpecialOffers = `SELECT *
|
||||
FROM account_offers offer
|
||||
LEFT JOIN products AS product ON product.product_sid = offer.product_sid
|
||||
WHERE offer.account_sid = ?`;
|
||||
|
||||
const combineProductAndPrice = (localProducts, product, prices) => {
|
||||
const lp = localProducts.find((lp) => lp.category === product.metadata.jambonz_category);
|
||||
return {
|
||||
product_sid: lp.product_sid,
|
||||
name: lp.name,
|
||||
category: lp.category,
|
||||
stripe_product_id: product.id,
|
||||
description: product.description,
|
||||
unit_label: product.unit_label,
|
||||
prices: prices.map((price) => {
|
||||
return {
|
||||
stripe_price_id: price.id,
|
||||
billing_scheme: price.billing_scheme,
|
||||
currency: price.currency,
|
||||
recurring: price.recurring,
|
||||
tiers_mode: price.tiers_mode,
|
||||
tiers: price.tiers,
|
||||
type: price.type,
|
||||
unit_amount: price.unit_amount,
|
||||
unit_amount_decimal: price.unit_amount_decimal
|
||||
};
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user || {};
|
||||
const {listProducts, retrieveProduct, retrievePricesForProduct} = require('../../utils/stripe-utils');
|
||||
try {
|
||||
const localProducts = await Product.retrieveAll();
|
||||
|
||||
/**
|
||||
* If this request is for a specific account (we have an account_sid)
|
||||
* then check to see if we have any special offers for this account
|
||||
*/
|
||||
const selectedProducts = [];
|
||||
if (account_sid) {
|
||||
const [r] = await promisePool.query({sql: sqlRetrieveSpecialOffers, nestTables: true}, account_sid);
|
||||
logger.debug({r}, `retrieved special offer ids for account_sid ${account_sid}`);
|
||||
|
||||
if (r.length > 0) {
|
||||
/* retrieve all the offers for this account */
|
||||
const products = await Promise.all(r.map((row) => retrieveProduct(logger, row.offer.stripe_product_id)));
|
||||
logger.debug({products}, `retrieved special offer products for account_sid ${account_sid}`);
|
||||
const prices = await Promise.all(products.map((prod) => retrievePricesForProduct(logger, prod.id)));
|
||||
logger.debug({prices}, `retrieved special offer prices for account_sid ${account_sid}`);
|
||||
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
selectedProducts.push(combineProductAndPrice(localProducts, products[i], prices[i].data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* we must return at least pricing for sessions and devices, so find and use
|
||||
* the general pricing if no account-specific product was specified for these
|
||||
*/
|
||||
const haveSessionPricing = selectedProducts.find((prod) => prod.category === 'voice_call_session');
|
||||
const haveDevicePricing = selectedProducts.find((prod) => prod.category === 'device');
|
||||
|
||||
if (haveSessionPricing && haveDevicePricing) {
|
||||
logger.debug({selectedProducts}, 'found account level offers for sessions and devices');
|
||||
return res.status(200).json(selectedProducts);
|
||||
}
|
||||
|
||||
/* need to get default pricing */
|
||||
const allProducts = await listProducts(logger);
|
||||
logger.debug({allProducts}, 'retrieved all products');
|
||||
const defaultProducts = allProducts.data.filter((prod) =>
|
||||
['voice_call_session', 'device'].includes(prod.metadata.jambonz_category) &&
|
||||
'general' === prod.metadata.availability);
|
||||
logger.debug({defaultProducts}, 'default products');
|
||||
|
||||
if (!haveSessionPricing) {
|
||||
const product = defaultProducts.find((prod) => 'voice_call_session' === prod.metadata.jambonz_category);
|
||||
if (product) {
|
||||
logger.debug(`retrieving prices for product id ${product.id}`);
|
||||
const prices = await retrievePricesForProduct(logger, product.id);
|
||||
selectedProducts.push(combineProductAndPrice(localProducts, product, prices.data));
|
||||
}
|
||||
}
|
||||
if (!haveDevicePricing) {
|
||||
const product = defaultProducts.find((prod) => 'device' === prod.metadata.jambonz_category);
|
||||
if (product) {
|
||||
logger.debug(`retrieving prices for product id ${product.id}`);
|
||||
const prices = await retrievePricesForProduct(logger, product.id);
|
||||
selectedProducts.push(combineProductAndPrice(localProducts, product, prices.data));
|
||||
}
|
||||
}
|
||||
res.status(200).json(selectedProducts);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,78 @@
|
||||
const assert = require('assert');
|
||||
const router = require('express').Router();
|
||||
const Product = require('../../models/product');
|
||||
const {listProducts, listPrices} = require('../../utils/stripe-utils');
|
||||
const sysError = require('./error');
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const [stripeProducts, localProducts] = await Promise.all([listProducts(), Product.retrieveAll()]);
|
||||
console.log(stripeProducts);
|
||||
console.log(localProducts);
|
||||
const arr = localProducts.map((p) => {
|
||||
const stripe = stripeProducts.data
|
||||
.find((s) => s.metadata.jambonz_category === p.category);
|
||||
assert.ok(stripe, `No stripe product found for category ${p.category}`);
|
||||
Object.assign(p, {
|
||||
stripe_product_id: stripe.id,
|
||||
statement_descriptor: stripe.statement_descriptor,
|
||||
description: stripe.description,
|
||||
unit_label: stripe.unit_label
|
||||
});
|
||||
return p;
|
||||
});
|
||||
res.status(200).json(arr);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* get */
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const [allPrices, results] = await Promise.all([listPrices(), Product.retrieve(req.params.sid)]);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const product = results[0];
|
||||
const prices = allPrices.data
|
||||
.filter((p) => p.active && p.product.active)
|
||||
.filter((p) => p.product.metadata.jambonz_category === product.category);
|
||||
assert(prices.length > 0, `No pricing data found for product ${req.params.sid}`);
|
||||
const stripe = prices[0].product;
|
||||
Object.assign(product, {
|
||||
stripe_product_id: stripe.id,
|
||||
statement_descriptor: stripe.statement_descriptor,
|
||||
description: stripe.description,
|
||||
unit_label: stripe.unit_label
|
||||
});
|
||||
|
||||
// get pricing
|
||||
Object.assign(product, {
|
||||
pricing: {
|
||||
billing_scheme: stripe.billing_scheme,
|
||||
type: prices[0].type
|
||||
}
|
||||
});
|
||||
|
||||
product.pricing.fees = prices.map((price) => {
|
||||
const obj = {
|
||||
stripe_price_id: price.id,
|
||||
currency: price.currency,
|
||||
unit_amount: price.unit_amount,
|
||||
unit_amount_decimal: price.unit_amount_decimal
|
||||
};
|
||||
if (price.tiers) {
|
||||
obj.tiers = price.tiers;
|
||||
obj.tiers_mode = price.tiers_mode;
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
res.status(200).json(product);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,37 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const {logger, queryCdrs} = req.app.locals;
|
||||
try {
|
||||
logger.debug({opts: req.query}, 'GET /RecentCalls');
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
|
||||
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
|
||||
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
const data = await queryCdrs({
|
||||
account_sid,
|
||||
page,
|
||||
page_size: count,
|
||||
trunk,
|
||||
direction,
|
||||
days,
|
||||
answered,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
});
|
||||
|
||||
res.status(200).json(data);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,344 @@
|
||||
const router = require('express').Router();
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
|
||||
const {validateEmail} = require('../../utils/email-utils');
|
||||
const uuid = require('uuid').v4;
|
||||
const short = require('short-uuid');
|
||||
const translator = short();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils');
|
||||
const {generateHashedPassword} = require('../../utils/password-utils');
|
||||
const sysError = require('../error');
|
||||
const insertUserSql = `INSERT into users
|
||||
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
|
||||
values (?, ?, ?, ?, ?, ?, 1)`;
|
||||
const insertUserLocalSql = `INSERT into users
|
||||
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
|
||||
values (?, ?, ?, ?, ?, 0, 'local', ?)`;
|
||||
const insertAccountSql = `INSERT into accounts
|
||||
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
|
||||
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
|
||||
const queryRootDomainSql = `SELECT root_domain
|
||||
FROM service_providers
|
||||
WHERE service_providers.service_provider_sid = ?`;
|
||||
const insertSignupHistorySql = `INSERT into signup_history
|
||||
(email, name)
|
||||
values (?, ?)`;
|
||||
|
||||
const addLocalUser = async(logger, user_sid, account_sid,
|
||||
name, email, email_activation_code, passwordHash) => {
|
||||
const [r] = await promisePool.execute(insertUserLocalSql,
|
||||
[
|
||||
user_sid,
|
||||
account_sid,
|
||||
name,
|
||||
email,
|
||||
email_activation_code,
|
||||
passwordHash
|
||||
]);
|
||||
debug({r}, 'Result from adding user');
|
||||
};
|
||||
const addOauthUser = async(logger, user_sid, account_sid,
|
||||
name, email, provider, provider_userid) => {
|
||||
const [r] = await promisePool.execute(insertUserSql,
|
||||
[
|
||||
user_sid,
|
||||
account_sid,
|
||||
name,
|
||||
email,
|
||||
provider,
|
||||
provider_userid
|
||||
]);
|
||||
logger.debug({r}, 'Result from adding user');
|
||||
};
|
||||
|
||||
const validateRequest = async(req, user_sid) => {
|
||||
const payload = req.body || {};
|
||||
|
||||
/* check required properties are there */
|
||||
['provider', 'service_provider_sid'].forEach((prop) => {
|
||||
if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop}`);
|
||||
});
|
||||
|
||||
/* valid service provider? */
|
||||
const [rows] = await promisePool.query('SELECT * from service_providers WHERE service_provider_sid = ?',
|
||||
payload.service_provider_sid);
|
||||
if (0 === rows.length) throw new DbErrorUnprocessableRequest('invalid service_provider_sid');
|
||||
|
||||
/* valid provider? */
|
||||
if (!['local', 'github', 'google', 'twitter'].includes(payload.provider)) {
|
||||
throw new DbErrorUnprocessableRequest(`invalid provider: ${payload.provider}`);
|
||||
}
|
||||
|
||||
/* if local provider then email/password */
|
||||
if ('local' === payload.provider) {
|
||||
if (!payload.email || !payload.password) throw new DbErrorBadRequest('missing email or password');
|
||||
|
||||
/* valid email? */
|
||||
if (!validateEmail(payload.email)) throw new DbErrorBadRequest('invalid email');
|
||||
|
||||
/* valid password? */
|
||||
if (payload.password.length < 6) throw new DbErrorBadRequest('password must be at least 6 characters');
|
||||
|
||||
/* is this email available? */
|
||||
if (user_sid) {
|
||||
const [rows] = await promisePool.query('SELECT * from users WHERE email = ? AND user_sid <> ?',
|
||||
[payload.email, user_sid]);
|
||||
if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email');
|
||||
}
|
||||
else {
|
||||
const [rows] = await promisePool.query('SELECT * from users WHERE email = ?', payload.email);
|
||||
if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email');
|
||||
}
|
||||
|
||||
/* verify that we have a code to email them */
|
||||
if (!payload.email_activation_code) throw new DbErrorBadRequest('email activation code required');
|
||||
}
|
||||
else {
|
||||
['oauth2_code', 'oauth2_state', 'oauth2_client_id', 'oauth2_redirect_uri'].forEach((prop) => {
|
||||
if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop} for provider ${payload.provider}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const parseAuthorizationToken = (logger, req) => {
|
||||
const notfound = {};
|
||||
const authHeader = req.get('Authorization');
|
||||
if (!authHeader) return Promise.resolve(notfound);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const arr = /^Bearer (.*)$/.exec(req.get('Authorization'));
|
||||
if (!arr) return resolve(notfound);
|
||||
jwt.verify(arr[1], process.env.JWT_SECRET, async(err, decoded) => {
|
||||
if (err) return resolve(notfound);
|
||||
logger.debug({jwt: decoded}, 'register - create new user for existing account');
|
||||
resolve(decoded);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* called to create a new user and account
|
||||
* or new user with existing account, in case of "change auth mechanism"
|
||||
*/
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, writeCdrs, writeAlerts, AlertType} = req.app.locals;
|
||||
const userProfile = {};
|
||||
|
||||
try {
|
||||
const {user_sid, account_sid} = await parseAuthorizationToken(logger, req);
|
||||
await validateRequest(req, user_sid);
|
||||
|
||||
logger.debug({payload: req.body}, 'POST /register');
|
||||
|
||||
if (req.body.provider === 'github') {
|
||||
const user = await doGithubAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details from github');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
email_validated: user.email_validated,
|
||||
avatar_url: user.avatar_url,
|
||||
provider: 'github',
|
||||
provider_userid: user.login
|
||||
});
|
||||
}
|
||||
else if (req.body.provider === 'google') {
|
||||
const user = await doGoogleAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details from google');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
email_validated: user.verified_email,
|
||||
picture: user.picture,
|
||||
provider: 'google',
|
||||
provider_userid: user.id
|
||||
});
|
||||
}
|
||||
else if (req.body.provider === 'local') {
|
||||
const user = await doLocalAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details for local provider');
|
||||
debug({user}, 'retrieved user details for local provider');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
provider: 'local',
|
||||
email_activation_code: user.email_activation_code
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.provider !== 'local') {
|
||||
/* when using oauth2, check to see if user already exists */
|
||||
const [users] = await promisePool.query(
|
||||
'SELECT * from users WHERE provider = ? AND provider_userid = ?',
|
||||
[userProfile.provider, userProfile.provider_userid]);
|
||||
logger.debug({users}, `Result from retrieving user for ${userProfile.provider}:${userProfile.provider_userid}`);
|
||||
if (1 === users.length) {
|
||||
|
||||
/* if changing existing account to oauth, no other user with that provider/userid must exist */
|
||||
if (user_sid) {
|
||||
throw new DbErrorUnprocessableRequest('account already exists for this oauth user/provider');
|
||||
}
|
||||
Object.assign(userProfile, {
|
||||
user_sid: users[0].user_sid,
|
||||
account_sid: users[0].account_sid,
|
||||
name: users[0].name,
|
||||
email: users[0].email,
|
||||
phone: users[0].phone,
|
||||
pristine: false,
|
||||
email_validated: users[0].email_validated ? true : false,
|
||||
phone_validated: users[0].phone_validated ? true : false,
|
||||
scope: users[0].scope
|
||||
});
|
||||
|
||||
const [accounts] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?',
|
||||
userProfile.account_sid);
|
||||
if (accounts.length === 0) throw new DbErrorUnprocessableRequest('user exists with no associated account');
|
||||
Object.assign(userProfile, {
|
||||
is_active: accounts[0].is_active == 1,
|
||||
tutorial_completion: accounts[0].tutorial_completion
|
||||
});
|
||||
}
|
||||
else {
|
||||
/* you can not register from the sign-in page */
|
||||
if (req.body.locationBeforeAuth === '/sign-in') {
|
||||
logger.debug('redirecting user to /register so they accept Ts & Cs');
|
||||
return res.status(404).json({msg: 'registering a new account not allowed from the sign-in page'});
|
||||
}
|
||||
/* new user, but check if we already have an account with that email */
|
||||
let sql = 'SELECT * from users WHERE email = ?';
|
||||
const args = [userProfile.email];
|
||||
if (user_sid) {
|
||||
sql += ' AND user_sid <> ?';
|
||||
args.push(user_sid);
|
||||
}
|
||||
logger.debug(`sql is ${sql}`);
|
||||
const [accounts] = await promisePool.execute(sql, args);
|
||||
if (accounts.length > 0) {
|
||||
throw new DbErrorBadRequest(`user already exists with email ${userProfile.email}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (userProfile.pristine !== false && !user_sid) {
|
||||
/* add a new user and account */
|
||||
/* get root domain */
|
||||
const [sp] = await promisePool.query(queryRootDomainSql, req.body.service_provider_sid);
|
||||
if (0 === sp.length) throw new Error(`service_provider not found for sid ${req.body.service_provider_sid}`);
|
||||
if (!sp[0].root_domain) {
|
||||
throw new Error(`root_domain missing for service provider ${req.body.service_provider_sid}`);
|
||||
}
|
||||
|
||||
userProfile.root_domain = sp[0].root_domain;
|
||||
userProfile.account_sid = uuid();
|
||||
userProfile.user_sid = uuid();
|
||||
|
||||
const [r1] = await promisePool.execute(insertAccountSql,
|
||||
[
|
||||
userProfile.account_sid,
|
||||
req.body.service_provider_sid,
|
||||
userProfile.name || userProfile.email,
|
||||
req.body.provider !== 'local',
|
||||
`wh_secret_${translator.generate()}`
|
||||
]);
|
||||
logger.debug({r1}, 'Result from adding account');
|
||||
|
||||
/* add to signup history */
|
||||
let isReturningUser = false;
|
||||
try {
|
||||
await promisePool.execute(insertSignupHistorySql,
|
||||
[userProfile.email, userProfile.name || userProfile.email]);
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
logger.info(`register: user is signing up for a second trial: ${userProfile.email}`);
|
||||
isReturningUser = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* write sample cdrs and alerts in test environment */
|
||||
if ('test' === process.env.NODE_ENV) {
|
||||
await createTestCdrs(writeCdrs, userProfile.account_sid);
|
||||
await createTestAlerts(writeAlerts, AlertType, userProfile.account_sid);
|
||||
logger.debug('added test data for cdrs and alerts');
|
||||
}
|
||||
/* assign starter set of products */
|
||||
await setupFreeTrial(logger, userProfile.account_sid, isReturningUser);
|
||||
|
||||
/* add a user for the account */
|
||||
if (req.body.provider === 'local') {
|
||||
/* hash password */
|
||||
debug(`salting password: ${req.body.password}`);
|
||||
const passwordHash = await generateHashedPassword(req.body.password);
|
||||
debug(`hashed password: ${passwordHash}`);
|
||||
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
|
||||
debug('added local user');
|
||||
}
|
||||
else {
|
||||
await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.provider,
|
||||
userProfile.provider_userid);
|
||||
}
|
||||
|
||||
Object.assign(userProfile, {
|
||||
pristine: true,
|
||||
is_active: req.body.provider !== 'local',
|
||||
email_validated: userProfile.provider !== 'local',
|
||||
phone_validated: false,
|
||||
tutorial_completion: 0,
|
||||
scope: 'read-write'
|
||||
});
|
||||
}
|
||||
else if (user_sid) {
|
||||
/* add a new user for existing account */
|
||||
userProfile.user_sid = uuid();
|
||||
userProfile.account_sid = account_sid;
|
||||
|
||||
/* changing auth mechanism, add user for existing account */
|
||||
logger.debug(`register - creating new user for existing account ${account_sid}`);
|
||||
if (req.body.provider === 'local') {
|
||||
/* hash password */
|
||||
const passwordHash = await generateHashedPassword(req.body.password);
|
||||
|
||||
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.email_activation_code,
|
||||
passwordHash);
|
||||
|
||||
/* note: we deactivate the old user once the new email is validated */
|
||||
}
|
||||
else {
|
||||
await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.provider,
|
||||
userProfile.provider_userid);
|
||||
|
||||
/* deactivate the old/replaced user */
|
||||
const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]);
|
||||
logger.debug({r}, 'register - removed old user');
|
||||
}
|
||||
}
|
||||
|
||||
// generate a json web token for this user
|
||||
const token = jwt.sign({
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid,
|
||||
email: userProfile.email,
|
||||
name: userProfile.name
|
||||
}, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
|
||||
logger.debug({
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid
|
||||
}, 'generated jwt');
|
||||
|
||||
res.json({jwt: token, ...userProfile});
|
||||
|
||||
} catch (err) {
|
||||
debug(err, 'Error');
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
+13
-2
@@ -1,7 +1,9 @@
|
||||
const router = require('express').Router();
|
||||
const Sbc = require('../../models/sbc');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
//const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
//const {promisePool} = require('../../db');
|
||||
|
||||
decorate(router, Sbc, ['add', 'delete']);
|
||||
|
||||
@@ -9,7 +11,16 @@ decorate(router, Sbc, ['add', 'delete']);
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await Sbc.retrieveAll(req.query.service_provider_sid);
|
||||
const service_provider_sid = req.query.service_provider_sid;
|
||||
/*
|
||||
if (req.user.hasAccountAuth) {
|
||||
const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid);
|
||||
if (0 === r.length) throw new Error('invalid account_sid');
|
||||
service_provider_sid = r[0].service_provider_sid;
|
||||
}
|
||||
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
|
||||
*/
|
||||
const results = await Sbc.retrieveAll(service_provider_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
|
||||
@@ -2,7 +2,10 @@ const router = require('express').Router();
|
||||
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
const sysError = require('./error');
|
||||
const Account = require('../../models/account');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./utils');
|
||||
const sysError = require('../error');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
'delete': noActiveAccounts
|
||||
@@ -16,6 +19,49 @@ async function noActiveAccounts(req, sid) {
|
||||
|
||||
decorate(router, ServiceProvider, ['delete'], preconditions);
|
||||
|
||||
router.use('/:sid/SpeechCredentials', hasServiceProviderPermissions, require('./speech-credentials'));
|
||||
router.use('/:sid/PredefinedCarriers', hasServiceProviderPermissions, require('./add-from-predefined-carrier'));
|
||||
router.get('/:sid/Accounts', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const results = await Account.retrieveAll(service_provider_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const results = await VoipCarrier.retrieveAllForSP(service_provider_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const uuid = await VoipCarrier.make({...req.body, service_provider_sid});
|
||||
res.status(201).json({sid: uuid});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
router.get(':sid/Acccounts', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const results = await Account.retrieveAll(service_provider_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* add */
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
const router = require('express').Router();
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const {verifyPassword} = require('../../utils/password-utils');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const sysError = require('../error');
|
||||
|
||||
const validateRequest = async(req) => {
|
||||
const {email, password} = req.body || {};
|
||||
|
||||
/* check required properties are there */
|
||||
if (!email || !password) throw new DbErrorBadRequest('missing email or password');
|
||||
};
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, retrieveKey} = req.app.locals;
|
||||
const {email, password, link} = req.body;
|
||||
let user;
|
||||
|
||||
try {
|
||||
if (link) {
|
||||
const key = `reset-link:${link}`;
|
||||
const user_sid = await retrieveKey(key);
|
||||
logger.debug({user_sid}, 'retrieved user from link');
|
||||
if (!user_sid) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
const [r] = await promisePool.query('SELECT * from users WHERE user_sid = ?', user_sid);
|
||||
if (0 === r.length) return res.sendStatus(404);
|
||||
user = r[0];
|
||||
}
|
||||
else {
|
||||
validateRequest(req);
|
||||
const [r] = await promisePool.query(
|
||||
'SELECT * from users WHERE email = ? AND provider=\'local\' AND email_validated=1', email);
|
||||
if (0 === r.length) return res.sendStatus(404);
|
||||
user = r[0];
|
||||
|
||||
//debug(`password presented is ${password} and hashed_password in db is ${user.hashed_password}`);
|
||||
const isCorrect = await verifyPassword(user.hashed_password, password);
|
||||
if (!isCorrect) return res.sendStatus(403);
|
||||
}
|
||||
logger.debug({user}, 'signin: retrieved user');
|
||||
|
||||
const [a] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', user.account_sid);
|
||||
if (a.length !== 1) throw new Error('database error - account not found for user');
|
||||
|
||||
const userProfile = Object.assign({}, {
|
||||
user_sid: user.user_sid,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
account_sid: user.account_sid,
|
||||
force_change: !!user.force_change,
|
||||
provider: user.provider,
|
||||
provider_userid: user.provider_userid,
|
||||
scope: user.scope,
|
||||
phone_validated: !!user.phone_validated,
|
||||
email_validated: !!user.email_validated
|
||||
}, {
|
||||
is_active: !!a[0].is_active,
|
||||
tutorial_completion: a[0].tutorial_completion,
|
||||
pristine: false
|
||||
});
|
||||
|
||||
// generate a json web token for this session
|
||||
const token = jwt.sign({
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid
|
||||
}, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
|
||||
logger.debug({
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid
|
||||
}, 'generated jwt');
|
||||
|
||||
res.json({jwt: token, ...userProfile});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,8 +1,53 @@
|
||||
const router = require('express').Router();
|
||||
const SipGateway = require('../../models/sip-gateway');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {};
|
||||
const sysError = require('../error');
|
||||
|
||||
decorate(router, SipGateway, ['*'], preconditions);
|
||||
const validate = async(req, sid) => {
|
||||
const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals;
|
||||
let voip_carrier_sid;
|
||||
|
||||
if (sid) {
|
||||
const gateway = await lookupSipGatewayBySid(sid);
|
||||
if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid');
|
||||
voip_carrier_sid = gateway.voip_carrier_sid;
|
||||
}
|
||||
else {
|
||||
voip_carrier_sid = req.body.voip_carrier_sid;
|
||||
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
if (req.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
if (carrier.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
'add': validate,
|
||||
'update': validate,
|
||||
'delete': validate
|
||||
};
|
||||
|
||||
decorate(router, SipGateway, ['add', 'retrieve', 'update', 'delete'], preconditions);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const voip_carrier_sid = req.query.voip_carrier_sid;
|
||||
try {
|
||||
if (!voip_carrier_sid) {
|
||||
logger.info('GET /SipGateways missing voip_carrier_sid param');
|
||||
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
|
||||
}
|
||||
const results = await SipGateway.retrieveForVoipCarrier(voip_carrier_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
const router = require('express').Router();
|
||||
const {promisePool} = require('../../db');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {createDnsRecords, deleteDnsRecords} = require('../../utils/dns-utils');
|
||||
const uuid = require('uuid').v4;
|
||||
const sysError = require('../error');
|
||||
const insertDnsRecords = `INSERT INTO dns_records
|
||||
(dns_record_sid, account_sid, record_type, record_id)
|
||||
VALUES `;
|
||||
|
||||
|
||||
router.post('/:sip_realm', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const account_sid = req.user.account_sid;
|
||||
const sip_realm = req.params.sip_realm;
|
||||
try {
|
||||
const arr = /(.*)\.(.*\..*)$/.exec(sip_realm);
|
||||
if (!arr) throw new DbErrorBadRequest(`invalid sip_realm: ${sip_realm}`);
|
||||
const subdomain = arr[1];
|
||||
const domain = arr[2];
|
||||
|
||||
/* update the account */
|
||||
const [r] = await promisePool.execute('UPDATE accounts set sip_realm = ? WHERE account_sid = ?',
|
||||
[sip_realm, account_sid]);
|
||||
if (r.affectedRows !== 1) throw new Error('failure updating accounts table with sip_realm value');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test' || process.env.DME_API_KEY) {
|
||||
/* update DNS provider */
|
||||
|
||||
/* retrieve sbc addresses */
|
||||
const [sbcs] = await promisePool.query('SELECT ipv4 from sbc_addresses');
|
||||
if (sbcs.length === 0) throw new Error('no SBC addresses provisioned in the database!');
|
||||
const ips = sbcs.map((s) => s.ipv4);
|
||||
|
||||
/* retrieve existing dns records */
|
||||
const [old_recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?',
|
||||
account_sid);
|
||||
|
||||
if (old_recs.length > 0) {
|
||||
/* remove existing records from the database and dns provider */
|
||||
await promisePool.query('DELETE from dns_records WHERE account_sid = ?', account_sid);
|
||||
|
||||
const deleted = await deleteDnsRecords(logger, domain, old_recs.map((r) => r.record_id));
|
||||
if (!deleted) {
|
||||
logger.error({old_recs, sip_realm, account_sid},
|
||||
'Failed to remove old dns records when changing sip_realm for account');
|
||||
}
|
||||
}
|
||||
|
||||
/* add the dns records */
|
||||
const records = await createDnsRecords(logger, domain, subdomain, ips);
|
||||
if (!records) throw new Error(`failure updating dns records for ${sip_realm}`);
|
||||
const values = records.map((r) => {
|
||||
return `('${uuid()}', '${account_sid}', '${r.type}', ${r.id})`;
|
||||
}).join(',');
|
||||
const sql = `${insertDnsRecords}${values};`;
|
||||
const [result] = await promisePool.execute(sql);
|
||||
if (result.affectedRows != records.length) throw new Error('failed inserting dns records');
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,53 @@
|
||||
const router = require('express').Router();
|
||||
const SmppGateway = require('../../models/smpp_gateway');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
|
||||
const validate = async(req, sid) => {
|
||||
const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals;
|
||||
let voip_carrier_sid;
|
||||
|
||||
if (sid) {
|
||||
const gateway = await lookupSmppGatewayBySid(sid);
|
||||
if (!gateway) throw new DbErrorBadRequest('invalid smpp_gateway_sid');
|
||||
voip_carrier_sid = gateway.voip_carrier_sid;
|
||||
}
|
||||
else {
|
||||
voip_carrier_sid = req.body.voip_carrier_sid;
|
||||
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
if (req.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
if (carrier.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
'add': validate,
|
||||
'update': validate,
|
||||
'delete': validate
|
||||
};
|
||||
|
||||
decorate(router, SmppGateway, ['add', 'retrieve', 'update', 'delete'], preconditions);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const voip_carrier_sid = req.query.voip_carrier_sid;
|
||||
try {
|
||||
if (!voip_carrier_sid) {
|
||||
logger.info('GET /SmppGateways missing voip_carrier_sid param');
|
||||
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
|
||||
}
|
||||
const results = await SmppGateway.retrieveForVoipCarrier(voip_carrier_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,30 @@
|
||||
const router = require('express').Router();
|
||||
const Smpp = require('../../models/smpp');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
//const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
//const {promisePool} = require('../../db');
|
||||
|
||||
decorate(router, Smpp, ['add', 'delete']);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = req.query.service_provider_sid;
|
||||
/*
|
||||
if (req.user.hasAccountAuth) {
|
||||
const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid);
|
||||
if (0 === r.length) throw new Error('invalid account_sid');
|
||||
service_provider_sid = r[0].service_provider_sid;
|
||||
}
|
||||
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
|
||||
*/
|
||||
const results = await Smpp.retrieveAll(service_provider_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,7 +2,7 @@ const router = require('express').Router();
|
||||
const request = require('request');
|
||||
const getProvider = require('../../utils/sms-provider');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
let idx = 0;
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const getProvider = require('../../utils/sms-provider');
|
||||
const sysError = require('./error');
|
||||
const sysError = require('../error');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const { logger } = req.app.locals;
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
const router = require('express').Router();
|
||||
const SpeechCredential = require('../../models/speech-credential');
|
||||
const sysError = require('../error');
|
||||
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
|
||||
const {parseAccountSid, parseServiceProviderSid} = require('./utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
testAwsTts,
|
||||
testAwsStt
|
||||
} = require('../../utils/speech-utils');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {use_for_stt, use_for_tts, vendor, service_key, access_key_id, secret_access_key, aws_region} = req.body;
|
||||
const {account_sid} = req.user;
|
||||
let service_provider_sid;
|
||||
if (!account_sid) {
|
||||
if (!req.user.hasServiceProviderAuth) {
|
||||
logger.error('POST /SpeechCredentials invalid credentials');
|
||||
return res.send(403);
|
||||
}
|
||||
service_provider_sid = parseServiceProviderSid(req);
|
||||
}
|
||||
try {
|
||||
let encrypted_credential;
|
||||
if (vendor === 'google') {
|
||||
let obj;
|
||||
if (!service_key) throw new DbErrorBadRequest('invalid json key: service_key is required');
|
||||
try {
|
||||
obj = JSON.parse(service_key);
|
||||
if (!obj.client_email || !obj.private_key) {
|
||||
throw new DbErrorBadRequest('invalid google service account key');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
throw new DbErrorBadRequest('invalid google service account key - not JSON');
|
||||
}
|
||||
encrypted_credential = encrypt(service_key);
|
||||
}
|
||||
else if (vendor === 'aws') {
|
||||
const data = JSON.stringify({
|
||||
aws_region: aws_region || 'us-east-1',
|
||||
access_key_id,
|
||||
secret_access_key
|
||||
});
|
||||
encrypted_credential = encrypt(data);
|
||||
}
|
||||
else throw new DbErrorBadRequest(`invalid speech vendor ${vendor}`);
|
||||
const uuid = await SpeechCredential.make({
|
||||
account_sid,
|
||||
service_provider_sid,
|
||||
vendor,
|
||||
use_for_tts,
|
||||
use_for_stt,
|
||||
credential: encrypted_credential
|
||||
});
|
||||
res.status(201).json({sid: uuid});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve all speech credentials for an account
|
||||
*/
|
||||
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 creds = account_sid ?
|
||||
await SpeechCredential.retrieveAll(account_sid) :
|
||||
await SpeechCredential.retrieveAllForSP(service_provider_sid);
|
||||
|
||||
res.status(200).json(creds.map((c) => {
|
||||
const {credential, ...obj} = c;
|
||||
if ('google' === obj.vendor) {
|
||||
obj.service_key = decrypt(credential);
|
||||
}
|
||||
else if ('aws' === obj.vendor) {
|
||||
const o = decrypt(credential);
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = o.secret_access_key;
|
||||
}
|
||||
return obj;
|
||||
}));
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve a specific speech credential
|
||||
*/
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const cred = await SpeechCredential.retrieve(sid);
|
||||
if (0 === cred.length) return res.sendStatus(404);
|
||||
const {credential, ...obj} = cred[0];
|
||||
if ('google' === obj.vendor) {
|
||||
obj.service_key = decrypt(credential);
|
||||
}
|
||||
else if ('aws' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = o.secret_access_key;
|
||||
}
|
||||
res.status(200).json(obj);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* delete a speech credential
|
||||
*/
|
||||
router.delete('/:sid', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const count = await SpeechCredential.remove(sid);
|
||||
if (0 === count) return res.sendStatus(404);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* update a speech credential -- we only allow use_for_tts and use_for_stt to be updated
|
||||
*/
|
||||
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} = 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');
|
||||
}
|
||||
const obj = {};
|
||||
if (typeof use_for_tts !== 'undefined') {
|
||||
obj.use_for_tts = use_for_tts;
|
||||
}
|
||||
if (typeof use_for_stt !== 'undefined') {
|
||||
obj.use_for_stt = use_for_stt;
|
||||
}
|
||||
|
||||
const rowsAffected = await SpeechCredential.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Test a credential
|
||||
*/
|
||||
router.get('/:sid/test', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const creds = await SpeechCredential.retrieve(sid);
|
||||
if (!creds || 0 === creds.length) return res.sendStatus(404);
|
||||
|
||||
const cred = creds[0];
|
||||
const credential = JSON.parse(decrypt(cred.credential));
|
||||
const results = {
|
||||
tts: {
|
||||
status: 'not tested'
|
||||
},
|
||||
stt: {
|
||||
status: 'not tested'
|
||||
}
|
||||
};
|
||||
if (cred.vendor === 'google') {
|
||||
if (!credential.client_email || !credential.private_key) {
|
||||
throw new DbErrorUnprocessableRequest('uploaded file is not a google service key');
|
||||
}
|
||||
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testGoogleTts(logger, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testGoogleStt(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 === 'aws') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testAwsTts(logger, {
|
||||
accessKeyId: credential.access_key_id,
|
||||
secretAccessKey: credential.secret_access_key,
|
||||
region: credential.aws_region || process.env.AWS_REGION
|
||||
});
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testAwsStt(logger, {
|
||||
accessKeyId: credential.access_key_id,
|
||||
secretAccessKey: credential.secret_access_key,
|
||||
region: credential.aws_region || process.env.AWS_REGION
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,48 @@
|
||||
const router = require('express').Router();
|
||||
const Account = require('../../models/account');
|
||||
const sysError = require('../error');
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const {createCustomer} = require('../../utils/stripe-utils');
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const {account_sid, email, name} = req.user;
|
||||
logger.debug({account_sid, email, name}, 'GET /StripeCustomerId');
|
||||
const results = await Account.retrieve(account_sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const account = results[0];
|
||||
|
||||
/* is account already provisioned in Stripe ? */
|
||||
if (account.stripe_customer_id) return res.status(200).json({stripe_customer_id: account.stripe_customer_id});
|
||||
|
||||
/* no - provision it now */
|
||||
const customer = await createCustomer(logger, account_sid, email, name);
|
||||
account.stripe_customer_id = customer.id;
|
||||
await Account.updateStripeCustomerId(account_sid, customer.id);
|
||||
res.status(200).json({stripe_customer_id: customer.id});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* delete */
|
||||
router.delete('/', async(req, res) => {
|
||||
const {deleteCustomer} = require('../../utils/stripe-utils');
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
try {
|
||||
const acc = await Account.retrieve(account_sid);
|
||||
logger.debug({acc}, 'retrieved account');
|
||||
if (!acc || 0 === acc.length || !acc[0].stripe_customer_id) return res.sendStatus(404);
|
||||
const {stripe_customer_id} = acc[0];
|
||||
logger.info(`deleting stripe customer id ${stripe_customer_id}`);
|
||||
await deleteCustomer(logger, stripe_customer_id);
|
||||
await Account.updateStripeCustomerId(account_sid, null);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,334 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
const {
|
||||
createCustomer,
|
||||
retrieveCustomer,
|
||||
updateCustomer,
|
||||
createSubscription,
|
||||
retrieveSubscription,
|
||||
updateSubscription,
|
||||
retrieveInvoice,
|
||||
payOutstandingInvoicesForCustomer,
|
||||
attachPaymentMethod,
|
||||
detachPaymentMethod,
|
||||
retrievePaymentMethod,
|
||||
retrieveUpcomingInvoice
|
||||
} = require('../../utils/stripe-utils');
|
||||
const {setupFreeTrial} = require('./utils');
|
||||
const sysError = require('../error');
|
||||
const actions = [
|
||||
'upgrade-to-paid',
|
||||
'downgrade-to-free',
|
||||
'update-payment-method',
|
||||
'update-quantities'
|
||||
];
|
||||
|
||||
const handleError = async(logger, method, res, err) => {
|
||||
if ('StatusError' === err.name) {
|
||||
const text = await err.text();
|
||||
let details;
|
||||
if (text) {
|
||||
details = JSON.parse(text);
|
||||
logger.info({details}, `${method} failed`);
|
||||
}
|
||||
if (402 === err.statusCode && details) {
|
||||
return res.status(err.statusCode).json(details);
|
||||
}
|
||||
return res.sendStatus(err.statusCode);
|
||||
}
|
||||
sysError(logger, res, err);
|
||||
};
|
||||
|
||||
/**
|
||||
* We handle 3 possible outcomes
|
||||
* - the initial payment was successful
|
||||
* - there was a card error on the initial payment (i.e. decline)
|
||||
* - there is a requirement for additional authentication (e.g. SCA)
|
||||
* see: https://stripe.com/docs/billing/migration/strong-customer-authentication
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} subscription
|
||||
*/
|
||||
const handleSubscriptionOutcome = async(req, res, subscription) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = subscription.metadata;
|
||||
const {status, latest_invoice} = subscription;
|
||||
const {payment_intent} = latest_invoice;
|
||||
|
||||
/* success case */
|
||||
if ('active' == status && 'paid' === latest_invoice.status && 'succeeded' === payment_intent.status) {
|
||||
await Account.activateSubscription(logger, account_sid, subscription.id, 'upgrade to paid plan');
|
||||
return res.status(201).json({
|
||||
status: 'success',
|
||||
chargedAmount: latest_invoice.amount_paid,
|
||||
currency: payment_intent.currency,
|
||||
statementDescriptor: payment_intent.statement_descriptor
|
||||
});
|
||||
}
|
||||
|
||||
/* card error */
|
||||
if ('incomplete' == status && 'open' === latest_invoice.status &&
|
||||
'requires_payment_method' === payment_intent.status) {
|
||||
return res.status(201).json({
|
||||
status: 'card error',
|
||||
subscription: subscription.id,
|
||||
client_secret: payment_intent.client_secret,
|
||||
reason: payment_intent.last_payment_error.message
|
||||
});
|
||||
}
|
||||
|
||||
/* more authentication required */
|
||||
if ('incomplete' == status && 'open' === latest_invoice.status && 'requires_action' === payment_intent.status) {
|
||||
return res.status(201).json({
|
||||
status: 'action required',
|
||||
subscription: subscription.id,
|
||||
client_secret: payment_intent.client_secret
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`handleSubscriptionOutcome unexpected status ${status}:${latest_invoice.status}:${payment_intent.status}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transition from free --> paid
|
||||
* Create customer in Stripe, if needed
|
||||
* Set the default payment method
|
||||
* Create a subscription
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
const upgradeToPaidPlan = async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid, name, email} = req.user;
|
||||
const {payment_method_id, products} = req.body;
|
||||
const arr = await Account.retrieve(req.user.account_sid);
|
||||
const account = arr[0];
|
||||
|
||||
/* retrieve stripe customer id locally, provision on Stripe if needed */
|
||||
logger.debug({account}, 'upgradeToPaidPlan retrieved account');
|
||||
let stripe_customer_id = account.stripe_customer_id;
|
||||
if (!stripe_customer_id) {
|
||||
logger.debug('upgradeToPaidPlan provisioning customer');
|
||||
const customer = await createCustomer(logger, account_sid, email, name);
|
||||
logger.debug(`upgradeToPaidPlan provisioned customer_id ${customer.id}`);
|
||||
await Account.updateStripeCustomerId(account_sid, customer.id);
|
||||
stripe_customer_id = customer.id;
|
||||
}
|
||||
|
||||
/* attach the payment method to the customer and make it their default */
|
||||
const pm = await attachPaymentMethod(logger, payment_method_id, stripe_customer_id);
|
||||
const customer = await updateCustomer(logger, stripe_customer_id, {
|
||||
invoice_settings: {
|
||||
default_payment_method: req.body.payment_method_id,
|
||||
}
|
||||
});
|
||||
logger.debug({customer}, 'successfully updated customer');
|
||||
|
||||
/* create a pending subscription -- will be activated on invoice.paid */
|
||||
const account_subscription_sid = await Account.provisionPendingSubscription(logger, account_sid, products, pm);
|
||||
|
||||
/* create the subscription in Stripe */
|
||||
const items = products.map((product) => {
|
||||
return {
|
||||
price: product.price_id,
|
||||
quantity: product.quantity,
|
||||
metadata: {
|
||||
product_sid: product.product_sid
|
||||
}
|
||||
};
|
||||
});
|
||||
logger.debug({items}, 'creating subscription');
|
||||
const subscription = await createSubscription(logger, stripe_customer_id,
|
||||
{account_sid, account_subscription_sid}, items);
|
||||
logger.debug({subscription}, 'created subscription');
|
||||
|
||||
await handleSubscriptionOutcome(req, res, subscription);
|
||||
};
|
||||
const downgradeToFreePlan = async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
try {
|
||||
await setupFreeTrial(logger, account_sid);
|
||||
return res.status(200).json({status: 'success'});
|
||||
} catch (err) {
|
||||
handleError(logger, 'downgradeToFreePlan', res, err);
|
||||
}
|
||||
};
|
||||
const updatePaymentMethod = async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
try {
|
||||
const {payment_method_id} = req.body;
|
||||
const arr = await Account.retrieve(req.user.account_sid);
|
||||
const account = arr[0];
|
||||
if (!account.stripe_customer_id) {
|
||||
throw new DbErrorBadRequest(`Account ${account_sid} is not provisioned in Stripe`);
|
||||
}
|
||||
const customer = await retrieveCustomer(logger, account.stripe_customer_id);
|
||||
//logger.debug({customer}, 'retrieved customer');
|
||||
|
||||
/* attach the payment method to the customer */
|
||||
const pm = await attachPaymentMethod(logger, payment_method_id, account.stripe_customer_id);
|
||||
logger.debug({pm}, 'attached payment method to customer');
|
||||
|
||||
/* update last4 etc in our db */
|
||||
await Account.updatePaymentInfo(logger, account_sid, pm);
|
||||
|
||||
/* make it the customer's default payment method */
|
||||
await updateCustomer(logger, account.stripe_customer_id, {
|
||||
invoice_settings: {
|
||||
default_payment_method: req.body.payment_method_id,
|
||||
}
|
||||
});
|
||||
|
||||
/* detach the customer's old payment method */
|
||||
const old_pm = customer.default_source || customer.invoice_settings.default_payment_method;
|
||||
if (old_pm) await detachPaymentMethod(logger, old_pm);
|
||||
|
||||
/* if the customer has an unpaid invoice, try to pay it */
|
||||
const success = await payOutstandingInvoicesForCustomer(logger, account.stripe_customer_id);
|
||||
res.status(200).json({
|
||||
status: success ? 'success' : 'failed to pay outstanding invoices'
|
||||
});
|
||||
} catch (err) {
|
||||
handleError(logger, 'updatePaymentMethod', res, err);
|
||||
}
|
||||
};
|
||||
const updateQuantities = async(req, res) => {
|
||||
/**
|
||||
* see https://stripe.com/docs/billing/subscriptions/upgrade-downgrade#immediate-payment
|
||||
* and https://stripe.com/docs/billing/subscriptions/pending-updates
|
||||
*/
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
const {products, dry_run} = req.body;
|
||||
|
||||
if (!products || !Array.isArray(products) ||
|
||||
0 === products.length ||
|
||||
products.find((p) => !p.price_id || !p.product_sid)) {
|
||||
logger.info({products}, 'Subscription:updateQuantities invalid products');
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
try {
|
||||
const account_subscription = await Account.getSubscription(req.user.account_sid);
|
||||
if (!account_subscription || !account_subscription.stripe_subscription_id) {
|
||||
logger.info(`Subscription:updateQuantities No active subscription found for account_sid ${account_sid}`);
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
const subscription_id = account_subscription.stripe_subscription_id;
|
||||
|
||||
const subscription = await retrieveSubscription(logger, subscription_id);
|
||||
logger.debug({subscription}, 'retrieved existing subscription');
|
||||
const pm = await retrievePaymentMethod(logger, account_subscription.stripe_payment_method_id);
|
||||
logger.debug({pm}, 'retrieved existing payment method');
|
||||
const items = products.map((product) => {
|
||||
const existingItem = subscription.items.data.find((i) => i.price.id === product.price_id);
|
||||
const obj = {
|
||||
quantity: product.quantity,
|
||||
};
|
||||
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id});
|
||||
});
|
||||
|
||||
if (dry_run) {
|
||||
const invoice = await retrieveUpcomingInvoice(logger, subscription.customer, subscription.id, items);
|
||||
logger.debug({invoice}, 'dry run - upcoming invoice');
|
||||
const dt = new Date(invoice.next_payment_attempt * 1000);
|
||||
const sum = (acc, current) => acc + current.amount;
|
||||
const prorated_cost = invoice.lines.data
|
||||
.filter((l) => l.proration === true)
|
||||
.reduce(sum, 0);
|
||||
const monthly_cost = invoice.lines.data
|
||||
.filter((l) => l.proration === false)
|
||||
.reduce(sum, 0);
|
||||
return res.status(201).json({
|
||||
currency: invoice.currency,
|
||||
prorated_cost,
|
||||
monthly_cost,
|
||||
next_invoice_date: dt.toDateString()
|
||||
});
|
||||
}
|
||||
|
||||
/* create a pending subscription */
|
||||
await Account.provisionPendingSubscription(logger, account_sid, products,
|
||||
pm, subscription_id);
|
||||
|
||||
/* update the subscription in Stripe */
|
||||
const updated = await updateSubscription(logger, subscription_id, items);
|
||||
logger.debug({updated}, 'updated subscription');
|
||||
|
||||
/* get latest invoice, to see if payment is needed */
|
||||
const invoice = await retrieveInvoice(logger, updated.latest_invoice);
|
||||
logger.debug({invoice}, 'latest invoice');
|
||||
|
||||
if ('paid' === invoice.status) {
|
||||
logger.debug('activating pending subscription to new quantities since no invoice outstanding');
|
||||
await Account.activateSubscription(logger, account_sid, subscription_id, 'selected new capacities');
|
||||
return res.status(201).json({
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
else {
|
||||
return res.status(201).json({
|
||||
status: 'failed',
|
||||
reason: 'payment required'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(logger, 'updatePaymentMethod', res, err);
|
||||
}
|
||||
};
|
||||
|
||||
/* create */
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const {action, payment_method_id, products} = req.body;
|
||||
|
||||
if (!actions.includes(action)) throw new DbErrorBadRequest('invalid or missing action');
|
||||
if ('update-payment-method' === action && typeof payment_method_id !== 'string') {
|
||||
throw new DbErrorBadRequest('missing payment_method_id');
|
||||
}
|
||||
if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) {
|
||||
throw new DbErrorBadRequest('missing products');
|
||||
}
|
||||
if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) {
|
||||
throw new DbErrorBadRequest('missing products');
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'upgrade-to-paid':
|
||||
await upgradeToPaidPlan(req, res);
|
||||
break;
|
||||
case 'downgrade-to-free':
|
||||
await downgradeToFreePlan(req, res);
|
||||
break;
|
||||
case 'update-payment-method':
|
||||
await updatePaymentMethod(req, res);
|
||||
break;
|
||||
case 'update-quantities':
|
||||
await updateQuantities(req, res);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(logger, 'POST /Subscription', res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* get */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
try {
|
||||
const subscription = await Account.getSubscription(account_sid);
|
||||
if (!subscription || !subscription.stripe_subscription_id) return res.sendStatus(404);
|
||||
const sub = await retrieveSubscription(logger, subscription.stripe_subscription_id);
|
||||
res.status(200).json(sub);
|
||||
} catch (err) {
|
||||
handleError(logger, 'GET /Subscription', res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+165
-81
@@ -1,95 +1,179 @@
|
||||
//const assert = require('assert');
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
const router = require('express').Router();
|
||||
const crypto = require('crypto');
|
||||
const {getMysqlConnection} = require('../../db');
|
||||
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const {decrypt} = require('../../utils/encrypt-decrypt');
|
||||
const sysError = require('../error');
|
||||
const retrieveMyDetails = `SELECT *
|
||||
FROM users user
|
||||
JOIN accounts AS account ON account.account_sid = user.account_sid
|
||||
LEFT JOIN service_providers as sp ON account.service_provider_sid = sp.service_provider_sid
|
||||
WHERE user.user_sid = ?`;
|
||||
const retrieveSql = 'SELECT * from users where user_sid = ?';
|
||||
const updateSql = 'UPDATE users set hashed_password = ?, salt = ?, force_change = false WHERE user_sid = ?';
|
||||
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
|
||||
const retrieveProducts = `SELECT *
|
||||
FROM account_products
|
||||
JOIN products ON account_products.product_sid = products.product_sid
|
||||
JOIN account_subscriptions ON account_products.account_subscription_sid = account_subscriptions.account_subscription_sid
|
||||
WHERE account_subscriptions.account_sid = ?
|
||||
AND account_subscriptions.effective_end_date IS NULL
|
||||
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 genRandomString = (len) => {
|
||||
return crypto.randomBytes(Math.ceil(len / 2))
|
||||
.toString('hex') /** convert to hexadecimal format */
|
||||
.slice(0, len); /** return required number of characters */
|
||||
};
|
||||
const validateRequest = async(user_sid, payload) => {
|
||||
const {old_password, new_password, name, email, email_activation_code} = payload;
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest('hex');
|
||||
return {
|
||||
salt:salt,
|
||||
passwordHash:value
|
||||
};
|
||||
};
|
||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||
if (r.length === 0) return null;
|
||||
const user = r[0];
|
||||
|
||||
const saltHashPassword = (userpassword) => {
|
||||
var salt = genRandomString(16); /** Gives us salt of length 16 */
|
||||
return sha512(userpassword, salt);
|
||||
};
|
||||
|
||||
router.put('/:user_sid', (req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {old_password, new_password} = req.body;
|
||||
if (!old_password || !new_password) {
|
||||
logger.info('Bad PUT to /Users is missing old_password or new password');
|
||||
return res.sendStatus(400);
|
||||
if ((old_password && !new_password) || (new_password && !old_password)) {
|
||||
throw new DbErrorBadRequest('new_password and old_password both required');
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
conn.query(retrieveSql, [req.params.user_sid], (err, results) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
if (0 === results.length) {
|
||||
logger.info(`Failed to find user with sid ${req.params.user_sid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if ((email && !email_activation_code) || (email_activation_code && !email)) {
|
||||
throw new DbErrorBadRequest('email and email_activation_code both required');
|
||||
}
|
||||
if (!name && !new_password && !email) throw new DbErrorBadRequest('no updates requested');
|
||||
|
||||
logger.info({results}, 'successfully retrieved user');
|
||||
const old_salt = results[0].salt;
|
||||
const old_hashed_password = results[0].hashed_password;
|
||||
return user;
|
||||
};
|
||||
|
||||
const {passwordHash} = sha512(old_password, old_salt);
|
||||
if (old_hashed_password !== passwordHash) return res.sendStatus(403);
|
||||
router.get('/me', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.user;
|
||||
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
const {salt, passwordHash} = saltHashPassword(new_password);
|
||||
conn.query(updateSql, [passwordHash, salt, req.params.user_sid], (err, r) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
if (0 === r.changedRows) {
|
||||
logger.error('Failed updating database with new password');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
conn.query(tokenSql, (err, tokenResults) => {
|
||||
conn.release();
|
||||
if (err) {
|
||||
logger.error({err}, 'Error getting db connection');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
if (0 === tokenResults.length) {
|
||||
logger.error('Database has no admin token provisioned...run reset_admin_password');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
res.json({user_sid: results[0].user_sid, token: tokenResults[0].token});
|
||||
});
|
||||
});
|
||||
});
|
||||
if (!user_sid) return res.sendStatus(403);
|
||||
|
||||
try {
|
||||
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
|
||||
logger.debug(r, 'retrieved user details');
|
||||
const payload = r[0];
|
||||
const {user, account, sp} = payload;
|
||||
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
|
||||
delete user[prop];
|
||||
});
|
||||
});
|
||||
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
|
||||
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
|
||||
account.root_domain = sp.root_domain;
|
||||
delete payload.sp;
|
||||
|
||||
/* get api keys */
|
||||
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
|
||||
payload.api_keys = keys.map((k) => {
|
||||
return {
|
||||
api_key_sid: k.api_key_sid,
|
||||
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
|
||||
token: k.token,
|
||||
last_used: k.last_used,
|
||||
created_at: k.created_at
|
||||
};
|
||||
});
|
||||
|
||||
/* get products */
|
||||
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
|
||||
if (!products.length || !products[0].account_subscriptions) {
|
||||
throw new Error('account is missing a subscription');
|
||||
}
|
||||
const account_subscription = products[0].account_subscriptions;
|
||||
payload.subscription = {
|
||||
status: 'active',
|
||||
account_subscription_sid: account_subscription.account_subscription_sid,
|
||||
start_date: account_subscription.effective_start_date,
|
||||
products: products.map((prd) => {
|
||||
return {
|
||||
name: prd.products.name,
|
||||
units: prd.products.unit_label,
|
||||
quantity: prd.account_products.quantity
|
||||
};
|
||||
})
|
||||
};
|
||||
if (account_subscription.pending) {
|
||||
Object.assign(payload.subscription, {
|
||||
status: 'suspended',
|
||||
suspend_reason: account_subscription.pending_reason
|
||||
});
|
||||
}
|
||||
const {
|
||||
last4,
|
||||
exp_month,
|
||||
exp_year,
|
||||
card_type,
|
||||
stripe_statement_descriptor
|
||||
} = account_subscription;
|
||||
if (last4) {
|
||||
const real_last4 = decrypt(last4);
|
||||
Object.assign(payload.subscription, {
|
||||
last4: real_last4,
|
||||
exp_month,
|
||||
exp_year,
|
||||
card_type,
|
||||
statement_descriptor: stripe_statement_descriptor
|
||||
});
|
||||
}
|
||||
|
||||
/* get static ips */
|
||||
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
|
||||
payload.static_ips = static_ips.map((r) => r.public_ipv4);
|
||||
|
||||
logger.debug({payload}, 'returning user details');
|
||||
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
const {old_password, new_password, name, email, email_activation_code} = req.body;
|
||||
|
||||
if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
try {
|
||||
const user = await validateRequest(user_sid, req.body);
|
||||
if (!user) return res.sendStatus(404);
|
||||
|
||||
if (new_password) {
|
||||
const old_hashed_password = user.hashed_password;
|
||||
|
||||
const isCorrect = await verifyPassword(old_hashed_password, old_password);
|
||||
if (!isCorrect) {
|
||||
//debug(`PUT /Users/:sid pwd ${old_password} does not match hash ${old_hashed_password}`);
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
const passwordHash = await generateHashedPassword(new_password);
|
||||
//debug(`updating hashed_password to ${passwordHash}`);
|
||||
const r = await promisePool.execute(updateSql, [passwordHash, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (name) {
|
||||
const r = await promisePool.execute('UPDATE users SET name = ? WHERE user_sid = ?', [name, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (email) {
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
|
||||
[email, email_activation_code, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
//TODO: send email with activation code
|
||||
}
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
const uuid = require('uuid').v4;
|
||||
const Account = require('../../models/account');
|
||||
const {promisePool} = require('../../db');
|
||||
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
|
||||
const freePlans = require('../../utils/free_plans');
|
||||
const insertAccountSubscriptionSql = `INSERT INTO account_subscriptions
|
||||
(account_subscription_sid, account_sid)
|
||||
values (?, ?)`;
|
||||
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
|
||||
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
|
||||
WHERE account_subscription_sid = ?`;
|
||||
|
||||
const setupFreeTrial = async(logger, account_sid, isReturningUser) => {
|
||||
const sid = uuid();
|
||||
|
||||
/* see if we have an existing subscription */
|
||||
const account_subscription = await Account.getSubscription(account_sid);
|
||||
const planType = account_subscription || isReturningUser ? 'free' : 'trial';
|
||||
logger.debug({account_subscription}, `setupFreeTrial: assigning ${account_sid} to ${planType} plan`);
|
||||
|
||||
/* create a subscription */
|
||||
await promisePool.execute(insertAccountSubscriptionSql, [sid, account_sid]);
|
||||
|
||||
/* add products to it */
|
||||
const [products] = await promisePool.query('SELECT * from products');
|
||||
const name2Product = new Map();
|
||||
products.forEach((p) => name2Product.set(p.category, p.product_sid));
|
||||
|
||||
await Promise.all(freePlans[planType].map((p) => {
|
||||
const data = {
|
||||
account_product_sid: uuid(),
|
||||
account_subscription_sid: sid,
|
||||
product_sid: name2Product.get(p.category),
|
||||
quantity: p.quantity
|
||||
};
|
||||
return promisePool.query('INSERT INTO account_products SET ?', data);
|
||||
}));
|
||||
logger.debug({products}, 'setupFreeTrial: added products');
|
||||
|
||||
/* disable the old subscription, if any */
|
||||
if (account_subscription) {
|
||||
const {
|
||||
account_subscription_sid,
|
||||
stripe_subscription_id,
|
||||
stripe_payment_method_id
|
||||
} = account_subscription;
|
||||
await promisePool.execute(replaceOldSubscriptionSql, [
|
||||
'downgraded to free plan', account_subscription_sid]);
|
||||
logger.debug('setupFreeTrial: deactivated previous plan');
|
||||
|
||||
const promises = [];
|
||||
if (stripe_subscription_id) {
|
||||
logger.debug(`setupFreeTrial: deactivating subscription ${stripe_subscription_id}`);
|
||||
promises.push(cancelSubscription(logger, stripe_subscription_id));
|
||||
}
|
||||
if (stripe_payment_method_id) {
|
||||
promises.push(detachPaymentMethod(logger, stripe_payment_method_id));
|
||||
}
|
||||
if (promises.length) await Promise.all(promises);
|
||||
}
|
||||
|
||||
/* update account.plan */
|
||||
await promisePool.execute(
|
||||
'UPDATE accounts SET plan_type = ? WHERE account_sid = ?',
|
||||
[planType, account_sid]);
|
||||
};
|
||||
|
||||
const createTestCdrs = async(writeCdrs, account_sid) => {
|
||||
const points = 2000;
|
||||
const data = [];
|
||||
const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000));
|
||||
const now = new Date();
|
||||
const increment = (now.getTime() - start.getTime()) / points;
|
||||
for (let i = 0 ; i < points; i++) {
|
||||
const attempted_at = new Date(start.getTime() + (i * increment));
|
||||
const failed = 0 === i % 5;
|
||||
data.push({
|
||||
call_sid: 'b6f48929-8e86-4d62-ae3b-64fb574d91f6',
|
||||
from: '15083084809',
|
||||
to: '18882349999',
|
||||
answered: !failed,
|
||||
sip_callid: '685cd008-0a66-4974-b37a-bdd6d9a3c4aa@192.168.1.100',
|
||||
sip_status: 200,
|
||||
duration: failed ? 0 : 45,
|
||||
attempted_at: attempted_at.getTime(),
|
||||
answered_at: attempted_at.getTime() + 3000,
|
||||
terminated_at: attempted_at.getTime() + 45000,
|
||||
termination_reason: 'caller hungup',
|
||||
host: '192.168.1.100',
|
||||
remote_host: '3.55.24.34',
|
||||
account_sid,
|
||||
direction: 0 === i % 2 ? 'inbound' : 'outbound',
|
||||
trunk: 0 === i % 2 ? 'twilio' : 'user'
|
||||
});
|
||||
}
|
||||
|
||||
await writeCdrs(data);
|
||||
|
||||
};
|
||||
|
||||
const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
|
||||
const points = 100;
|
||||
const data = [];
|
||||
const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000));
|
||||
const now = new Date();
|
||||
const increment = (now.getTime() - start.getTime()) / points;
|
||||
for (let i = 0 ; i < points; i++) {
|
||||
const timestamp = new Date(start.getTime() + (i * increment));
|
||||
const scenario = i % 5;
|
||||
switch (scenario) {
|
||||
case 0:
|
||||
data.push({timestamp, account_sid,
|
||||
alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: 'http://foo.bar', status: 404});
|
||||
break;
|
||||
case 1:
|
||||
data.push({timestamp, account_sid, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: 'http://foo.bar'});
|
||||
break;
|
||||
case 2:
|
||||
data.push({timestamp, account_sid, alert_type: AlertType.TTS_NOT_PROVISIONED, vendor: 'google'});
|
||||
break;
|
||||
case 3:
|
||||
data.push({timestamp, account_sid, alert_type: AlertType.CARRIER_NOT_PROVISIONED});
|
||||
break;
|
||||
case 4:
|
||||
data.push({timestamp, account_sid, alert_type: AlertType.CALL_LIMIT, count: 50});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await writeAlerts(data);
|
||||
|
||||
};
|
||||
|
||||
const parseServiceProviderSid = (req) => {
|
||||
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
const parseAccountSid = (req) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
const hasAccountPermissions = (req, res, next) => {
|
||||
if (req.user.hasScope('admin')) return next();
|
||||
if (req.user.hasScope('account')) {
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (account_sid === req.user.account_sid) return next();
|
||||
}
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
const hasServiceProviderPermissions = (req, res, next) => {
|
||||
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'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupFreeTrial,
|
||||
createTestCdrs,
|
||||
createTestAlerts,
|
||||
parseAccountSid,
|
||||
parseServiceProviderSid,
|
||||
hasAccountPermissions,
|
||||
hasServiceProviderPermissions
|
||||
};
|
||||
@@ -1,15 +1,18 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const {promisePool} = require('../../db');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
'add': validate,
|
||||
'update': validate,
|
||||
'delete': noActiveAccounts
|
||||
};
|
||||
const sysError = require('../error');
|
||||
|
||||
async function validate(req) {
|
||||
const validate = async(req) => {
|
||||
const {lookupAppBySid, lookupAccountBySid} = req.app.locals;
|
||||
|
||||
/* account level user can only act on carriers associated to his/her account */
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
}
|
||||
|
||||
if (req.body.application_sid && !req.body.account_sid) {
|
||||
throw new DbErrorBadRequest('account_sid missing');
|
||||
}
|
||||
@@ -24,14 +27,71 @@ async function validate(req) {
|
||||
const account = await lookupAccountBySid(req.body.account_sid);
|
||||
if (!account) throw new DbErrorBadRequest('unknown account_sid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* can not delete a voip provider if it has any active phone numbers */
|
||||
async function noActiveAccounts(req, sid) {
|
||||
const validateUpdate = async(req, sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
await validate(req);
|
||||
|
||||
if (req.user.hasAccountAuth) {
|
||||
/* can only update carriers for the user's account */
|
||||
const carrier = await lookupCarrierBySid(sid);
|
||||
if (carrier.account_sid != req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('carrier belongs to a different user');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateDelete = async(req, sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (req.user.hasAccountAuth) {
|
||||
/* can only update carriers for the user's account */
|
||||
const carrier = await lookupCarrierBySid(sid);
|
||||
if (carrier.account_sid != req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('carrier belongs to a different user');
|
||||
}
|
||||
}
|
||||
|
||||
/* can not delete a voip provider if it has any active phone numbers */
|
||||
const activeAccounts = await VoipCarrier.getForeignKeyReferences('phone_numbers.voip_carrier_sid', sid);
|
||||
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete voip carrier with active phone numbers');
|
||||
}
|
||||
|
||||
decorate(router, VoipCarrier, ['*'], preconditions);
|
||||
/* remove all the sip and smpp gateways from the carrier first */
|
||||
await promisePool.execute('DELETE FROM sip_gateways WHERE voip_carrier_sid = ?', [sid]);
|
||||
await promisePool.execute('DELETE FROM smpp_gateways WHERE voip_carrier_sid = ?', [sid]);
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
'add': validate,
|
||||
'update': validateUpdate,
|
||||
'delete': validateDelete
|
||||
};
|
||||
|
||||
decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* retrieve */
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await VoipCarrier.retrieve(req.params.sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const router = require('express').Router();
|
||||
const Webhook = require('../../models/webhook');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
|
||||
decorate(router, Webhook, ['add']);
|
||||
|
||||
/* retrieve */
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await Webhook.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,24 @@
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../utils/errors');
|
||||
|
||||
function sysError(logger, res, err) {
|
||||
if (err instanceof DbErrorBadRequest) {
|
||||
logger.info(err, 'invalid client request');
|
||||
return res.status(400).json({msg: err.message});
|
||||
}
|
||||
if (err instanceof DbErrorUnprocessableRequest) {
|
||||
logger.info(err, 'unprocessable request');
|
||||
return res.status(422).json({msg: err.message});
|
||||
}
|
||||
if (err instanceof DbErrorForbidden) {
|
||||
logger.info(err, 'forbidden');
|
||||
return res.status(403).json({msg: err.message});
|
||||
}
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
logger.info(err, 'duplicate entry on insert');
|
||||
return res.status(422).json({msg: err.message});
|
||||
}
|
||||
logger.error(err, 'Database error');
|
||||
res.status(500).json({msg: err.message});
|
||||
}
|
||||
|
||||
module.exports = sysError;
|
||||
@@ -4,10 +4,12 @@ const YAML = require('yamljs');
|
||||
const path = require('path');
|
||||
const swaggerDocument = YAML.load(path.resolve(__dirname, '../swagger/swagger.yaml'));
|
||||
const api = require('./api');
|
||||
const stripe = require('./stripe');
|
||||
|
||||
const routes = express.Router();
|
||||
|
||||
routes.use('/v1', api);
|
||||
routes.use('/stripe', stripe);
|
||||
routes.use('/swagger', swaggerUi.serve);
|
||||
routes.get('/swagger', swaggerUi.setup(swaggerDocument));
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
|
||||
router.use('/webhook', require('./webhook'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,71 @@
|
||||
const router = require('express').Router();
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
const Account = require('../../models/account');
|
||||
const {retrieveSubscription} = require('../../utils/stripe-utils');
|
||||
const stripeFactory = require('stripe');
|
||||
const express = require('express');
|
||||
const sysError = require('../error');
|
||||
|
||||
/** Invoice events */
|
||||
const handleInvoicePaymentSucceeded = async(logger, obj) => {
|
||||
const {subscription} = obj;
|
||||
logger.debug({obj}, `payment for ${obj.billing_reason} succeeded`);
|
||||
const sub = await retrieveSubscription(logger, subscription);
|
||||
if ('active' === sub.status) {
|
||||
const {account_sid} = sub.metadata;
|
||||
if (await Account.activateSubscription(logger, account_sid, sub.id,
|
||||
'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) {
|
||||
logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Two cases:
|
||||
* (1) A subscription renewal fails. In this case we deactivate subscription
|
||||
* and the customer is down until they provide payment.
|
||||
* (2) A customer adds capacity during the month, and the pro-rated amount fails.
|
||||
* In this case, we leave the new subscription in a pending state
|
||||
* The customer continues (for the rest of the month at least) at
|
||||
* previous capacity levels.
|
||||
*/
|
||||
|
||||
const handleInvoicePaymentFailed = async(logger, obj) => {
|
||||
const {subscription} = obj;
|
||||
const sub = await retrieveSubscription(logger, subscription);
|
||||
logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`);
|
||||
const {account_sid} = sub.metadata;
|
||||
if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) {
|
||||
logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInvoiceEvents = async(logger, evt) => {
|
||||
if (evt.type === 'invoice.payment_succeeded') handleInvoicePaymentSucceeded(logger, evt.data.object);
|
||||
else if (evt.type === 'invoice.payment_failed') handleInvoicePaymentFailed(logger, evt.data.object);
|
||||
};
|
||||
|
||||
|
||||
router.post('/', express.raw({type: 'application/json'}), async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const sig = req.get('stripe-signature');
|
||||
|
||||
let evt;
|
||||
try {
|
||||
if (!process.env.STRIPE_WEBHOOK_SECRET) throw new Error('missing webhook secret');
|
||||
const stripe = stripeFactory(process.env.STRIPE_API_KEY);
|
||||
evt = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
/* process event */
|
||||
logger.info(`received webhook: ${evt.type}`);
|
||||
if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
|
||||
else {
|
||||
logger.debug(evt, 'unhandled stripe webook');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+2040
-34
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
if (!process.env.JAMBONES_HOSTING) return;
|
||||
|
||||
const bent = require('bent');
|
||||
const crypto = require('crypto');
|
||||
const assert = require('assert');
|
||||
const domains = new Map();
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
|
||||
const checkAsserts = () => {
|
||||
assert.ok(process.env.DME_API_KEY, 'missing env DME_API_KEY for dns operations');
|
||||
assert.ok(process.env.DME_API_SECRET, 'missing env DME_API_SECRET for dns operations');
|
||||
assert.ok(process.env.DME_BASE_URL, 'missing env DME_BASE_URL for dns operations');
|
||||
};
|
||||
|
||||
const createAuthHeaders = () => {
|
||||
const now = (new Date()).toUTCString();
|
||||
const hash = crypto.createHmac('SHA1', process.env.DME_API_SECRET);
|
||||
hash.update(now);
|
||||
return {
|
||||
'x-dnsme-apiKey': process.env.DME_API_KEY,
|
||||
'x-dnsme-requestDate': now,
|
||||
'x-dnsme-hmac': hash.digest('hex')
|
||||
};
|
||||
};
|
||||
|
||||
const getDnsDomainId = async(logger, name) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers);
|
||||
try {
|
||||
const result = await get('/dns/managed');
|
||||
debug(result, 'getDnsDomainId: all domains');
|
||||
if (Array.isArray(result.data)) {
|
||||
const domain = result.data.find((o) => o.name === name);
|
||||
if (domain) return domain.id;
|
||||
debug(`getDnsDomainId: failed to find domain ${name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the DNS records for a given subdomain
|
||||
* We will add an A record and an SRV record for each SBC public IP address
|
||||
* Note: this assumes we have manually added DNS A records:
|
||||
* sbc01.root.domain, sbc0.root.domain, etc to dnsmadeeasy
|
||||
*/
|
||||
const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
checkAsserts();
|
||||
try {
|
||||
if (!domains.has(domain)) {
|
||||
const domainId = await getDnsDomainId(logger, domain);
|
||||
if (!domainId) return false;
|
||||
domains.set(domain, domainId);
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
const a_records = value.map((v) => {
|
||||
return {
|
||||
type: 'A',
|
||||
gtdLocation: 'DEFAULT',
|
||||
name,
|
||||
value: v,
|
||||
ttl
|
||||
};
|
||||
});
|
||||
const srv_records = [
|
||||
{
|
||||
type: 'SRV',
|
||||
gtdLocation: 'DEFAULT',
|
||||
name: `_sip._udp.${name}`,
|
||||
value: `${name}`,
|
||||
port: 5060,
|
||||
priority: 10,
|
||||
weight: 100,
|
||||
ttl
|
||||
}
|
||||
];
|
||||
const headers = createAuthHeaders();
|
||||
const records = [...a_records, ...srv_records];
|
||||
const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers);
|
||||
logger.debug({records}, 'Attemting to create dns records');
|
||||
const res = await post(`/dns/managed/${domainId}/records/createMulti`,
|
||||
[...a_records, ...srv_records]);
|
||||
|
||||
if (201 === res.statusCode) {
|
||||
const str = await res.text();
|
||||
return JSON.parse(str);
|
||||
}
|
||||
logger.error({res}, 'Error creating records');
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDnsRecords = async(logger, domain, recIds) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers);
|
||||
try {
|
||||
if (!domains.has(domain)) {
|
||||
const domainId = await getDnsDomainId(logger, domain);
|
||||
if (!domainId) return false;
|
||||
domains.set(domain, domainId);
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
|
||||
await del(url);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error({err}, 'Error deleting records');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
getDnsDomainId,
|
||||
createDnsRecords,
|
||||
deleteDnsRecords
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const formData = require('form-data');
|
||||
const Mailgun = require('mailgun.js');
|
||||
const mailgun = new Mailgun(formData);
|
||||
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,}))$/;
|
||||
return re.test(email);
|
||||
};
|
||||
|
||||
const emailSimpleText = async(logger, to, subject, text) => {
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY
|
||||
});
|
||||
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
|
||||
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
|
||||
|
||||
try {
|
||||
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
|
||||
from: 'jambonz Support <support@jambonz.org>',
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({res}, 'sent email');
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error sending email');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateEmail,
|
||||
emailSimpleText
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const crypto = require('crypto');
|
||||
const algorithm = 'aes-256-ctr';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secretKey = crypto.createHash('sha256')
|
||||
.update(String(process.env.JWT_SECRET))
|
||||
.digest('base64')
|
||||
.substr(0, 32);
|
||||
|
||||
const encrypt = (text) => {
|
||||
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
||||
const data = {
|
||||
iv: iv.toString('hex'),
|
||||
content: encrypted.toString('hex')
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
const decrypt = (data) => {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
+8
-1
@@ -16,8 +16,15 @@ class DbErrorUnprocessableRequest extends DbError {
|
||||
}
|
||||
}
|
||||
|
||||
class DbErrorForbidden extends DbError {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"trial": [
|
||||
{
|
||||
"category": "voice_call_session",
|
||||
"quantity": 20
|
||||
},
|
||||
{
|
||||
"category": "device",
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"free": [
|
||||
{
|
||||
"category": "voice_call_session",
|
||||
"quantity": 1
|
||||
},
|
||||
{
|
||||
"category": "device",
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
const assert = require('assert');
|
||||
const bent = require('bent');
|
||||
const postJSON = bent('POST', 'json', 200);
|
||||
const getJSON = bent('GET', 'json', 200);
|
||||
const {emailSimpleText} = require('./email-utils');
|
||||
const {DbErrorForbidden} = require('../utils/errors');
|
||||
|
||||
const doGithubAuth = async(logger, payload) => {
|
||||
assert.ok(process.env.GITHUB_CLIENT_SECRET, 'env var GITHUB_CLIENT_SECRET is required');
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://github.com/login/oauth/access_token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri
|
||||
});
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
throw new Error(obj.error || 'error retrieving access_token');
|
||||
}
|
||||
logger.debug({obj}, 'got response from github for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://api.github.com/user', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
|
||||
const emails = await getJSON('https://api.github.com/user/emails', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
const primary = emails.find((e) => e.primary);
|
||||
if (primary) Object.assign(userDetails, {
|
||||
email: primary.email,
|
||||
email_validated: primary.validated
|
||||
});
|
||||
|
||||
logger.info({userDetails}, 'retrieved user details from github');
|
||||
return userDetails;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error authenticating via github');
|
||||
throw new DbErrorForbidden(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const doGoogleAuth = async(logger, payload) => {
|
||||
assert.ok(process.env.GOOGLE_OAUTH_CLIENT_SECRET, 'env var GOOGLE_OAUTH_CLIENT_SECRET is required');
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://oauth2.googleapis.com/token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri,
|
||||
grant_type: 'authorization_code'
|
||||
});
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
throw new Error(obj.error || 'error retrieving access_token');
|
||||
}
|
||||
logger.debug({obj}, 'got response from google for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
|
||||
logger.info({userDetails}, 'retrieved user details from google');
|
||||
return userDetails;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error authenticating via google');
|
||||
throw new DbErrorForbidden(err.message);
|
||||
}
|
||||
};
|
||||
const doLocalAuth = async(logger, payload) => {
|
||||
const {name, email, password, email_activation_code} = payload;
|
||||
const text = `Hi there
|
||||
|
||||
Welcome to jambonz! Your account activation code is ${email_activation_code}
|
||||
|
||||
Best,
|
||||
|
||||
The jambonz team`;
|
||||
|
||||
if ('test' !== process.env.NODE_ENV || process.env.MAILGUN_API_KEY) {
|
||||
await emailSimpleText(logger, email, 'Account activation code', text);
|
||||
}
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
email_activation_code
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
doGithubAuth,
|
||||
doGoogleAuth,
|
||||
doLocalAuth
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const crypto = require('crypto');
|
||||
const { argon2i } = require('argon2-ffi');
|
||||
const util = require('util');
|
||||
|
||||
const getRandomBytes = util.promisify(crypto.randomBytes);
|
||||
|
||||
const generateHashedPassword = async(password) => {
|
||||
const salt = await getRandomBytes(32);
|
||||
const passwordHash = await argon2i.hash(password, salt);
|
||||
return passwordHash;
|
||||
};
|
||||
|
||||
const verifyPassword = async(passwordHash, password) => {
|
||||
const isCorrect = await argon2i.verify(passwordHash, password);
|
||||
return isCorrect;
|
||||
};
|
||||
|
||||
const hashString = (s) => crypto.createHash('md5').update(s).digest('hex');
|
||||
|
||||
module.exports = {
|
||||
generateHashedPassword,
|
||||
verifyPassword,
|
||||
hashString
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
//const PNF = require('google-libphonenumber').PhoneNumberFormat;
|
||||
//const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||
|
||||
const validateNumber = (number) => {
|
||||
if (typeof number !== 'string') throw new Error('phone number must be a string');
|
||||
if (!/^\d+$/.test(number)) throw new Error('phone number must only include digits');
|
||||
};
|
||||
|
||||
const e164 = (number) => {
|
||||
if (number.startsWith('+')) return number.slice(1);
|
||||
return number;
|
||||
/*
|
||||
const num = phoneUtil.parseAndKeepRawInput(number, 'US');
|
||||
if (!phoneUtil.isValidNumber(num)) throw new Error(`not a valid US telephone number: ${number}`);
|
||||
return phoneUtil.format(num, PNF.E164).slice(1);
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateNumber,
|
||||
e164
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
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 fs = require('fs');
|
||||
|
||||
const testGoogleTts = async(logger, credentials) => {
|
||||
const client = new ttsGoogle.TextToSpeechClient({credentials});
|
||||
await client.listVoices();
|
||||
};
|
||||
|
||||
const testGoogleStt = async(logger, credentials) => {
|
||||
const client = new sttGoogle.SpeechClient({credentials});
|
||||
const config = {
|
||||
sampleRateHertz: 8000,
|
||||
languageCode: 'en-US',
|
||||
model: 'default',
|
||||
};
|
||||
const audio = {
|
||||
content: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`).toString('base64'),
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
audio: audio,
|
||||
};
|
||||
|
||||
// Detects speech in the audio file
|
||||
const [response] = await client.recognize(request);
|
||||
if (!Array.isArray(response.results) || 0 === response.results.length) {
|
||||
throw new Error('failed to transcribe speech');
|
||||
}
|
||||
};
|
||||
|
||||
const testAwsTts = (logger, credentials) => {
|
||||
const polly = new Polly(credentials);
|
||||
return new Promise((resolve, reject) => {
|
||||
polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const testAwsStt = (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();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
testAwsTts,
|
||||
testAwsStt
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
if (!process.env.JAMBONES_HOSTING) return;
|
||||
|
||||
const assert = require('assert');
|
||||
assert.ok(process.env.STRIPE_API_KEY || process.env.NODE_ENV === 'test',
|
||||
'missing env STRIPE_API_KEY for billing operations');
|
||||
assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test',
|
||||
'missing env STRIPE_BASE_URL for billing operations');
|
||||
|
||||
const bent = require('bent');
|
||||
const formurlencoded = require('form-urlencoded').default;
|
||||
const qs = require('qs');
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
const basicAuth = () => {
|
||||
const header = `Basic ${toBase64(process.env.STRIPE_API_KEY)}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
const postForm = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'POST', 'string',
|
||||
Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, basicAuth()), 200);
|
||||
const getJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'GET', 'json', basicAuth(), 200);
|
||||
const deleteJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'DELETE', 'json', basicAuth(), 200);
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
|
||||
const listProducts = async(logger) => await getJSON('/products?active=true');
|
||||
|
||||
const listPrices = async(logger) => await getJSON('/prices?active=true&expand[]=data.tiers&expand[]=data.product');
|
||||
|
||||
const retrievePricesForProduct = async(logger, id) =>
|
||||
await getJSON(`/prices?product=${id}&active=true&expand[]=data.tiers&expand[]=data.product`);
|
||||
|
||||
const retrieveProduct = async(logger, id) =>
|
||||
await getJSON(`/products/${id}`);
|
||||
|
||||
|
||||
const retrieveCustomer = async(logger, id) =>
|
||||
await getJSON(`/customers/${id}`);
|
||||
|
||||
const retrieveUpcomingInvoice = async(logger, customer_id, subscription_id, items) => {
|
||||
const params = Object.assign(
|
||||
{customer: customer_id},
|
||||
subscription_id ? {subscription: subscription_id} : {},
|
||||
items ? {subscription_items: items} : {});
|
||||
const queryString = qs.stringify(params, {encode: false});
|
||||
logger.debug({params, qs}, 'retrieving upcoming invoice');
|
||||
return await getJSON(`/invoices/upcoming?${queryString}`);
|
||||
};
|
||||
|
||||
const retrieveInvoice = async(logger, id) =>
|
||||
await getJSON(`/invoices/${id}`);
|
||||
|
||||
const createCustomer = async(logger, account_sid, email, name) => {
|
||||
const obj = {
|
||||
email,
|
||||
metadata: {account_sid}
|
||||
};
|
||||
if (name) obj.name = name;
|
||||
logger.debug({obj}, 'provisioning customer');
|
||||
const result = await postForm('/customers', formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
const updateCustomer = async(logger, id, obj) => {
|
||||
logger.debug({obj}, `updating customer ${id}`);
|
||||
const result = await postForm(`/customers/${id}`, formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
const deleteCustomer = async(logger, id) =>
|
||||
await deleteJSON(`/customers/${id}`);
|
||||
|
||||
const attachPaymentMethod = async(logger, payment_method_id, customer_id) => {
|
||||
const obj = {
|
||||
customer: customer_id
|
||||
};
|
||||
const result = await postForm(`/payment_methods/${payment_method_id}/attach`,
|
||||
formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
const detachPaymentMethod = async(logger, payment_method_id) => {
|
||||
const result = await postForm(`/payment_methods/${payment_method_id}/detach`);
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
const createSubscription = async(logger, customer, metadata, items) => {
|
||||
assert.ok(Array.isArray(items) && items.length > 0);
|
||||
const obj = {
|
||||
customer,
|
||||
metadata,
|
||||
items
|
||||
};
|
||||
const result = await postForm('/subscriptions?expand[]=latest_invoice&expand[]=latest_invoice.payment_intent',
|
||||
formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
/*
|
||||
const deleteInvoiceItem = async(logger, id) => {
|
||||
return JSON.parse(await deleteJSON(`/invoiceitems/${id}`));
|
||||
};
|
||||
*/
|
||||
|
||||
const updateSubscription = async(logger, id, items) => {
|
||||
assert.ok(Array.isArray(items) && items.length > 0);
|
||||
const obj = {
|
||||
proration_behavior: 'always_invoice',
|
||||
payment_behavior: 'pending_if_incomplete',
|
||||
items
|
||||
};
|
||||
const result = await postForm(`/subscriptions/${id}`,
|
||||
formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
|
||||
const payInvoice = async(logger, id) => {
|
||||
const result = await postForm(`/invoices/${id}/pay`);
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
const payOutstandingInvoicesForCustomer = async(logger, customer_id) => {
|
||||
let success = true;
|
||||
const customer = await retrieveCustomer(logger, customer_id);
|
||||
const {subscriptions} = customer;
|
||||
logger.debug({subscriptions}, 'payOutstandingInvoicesForCustomer - subscriptions');
|
||||
if (subscriptions && subscriptions.data.length > 0) {
|
||||
const promises = subscriptions.data
|
||||
.filter((s) => ['incomplete', 'past_due'].includes(s.status) || s.pending_update)
|
||||
.map((s) => payInvoice(logger, s.latest_invoice));
|
||||
const invoices = await Promise.all(promises);
|
||||
if (invoices.find((i) => 'paid' !== i.status)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
const retrieveSubscription = async(logger, id) =>
|
||||
await getJSON(`/subscriptions/${id}?expand[]=latest_invoice`);
|
||||
|
||||
const cancelSubscription = async(logger, id) =>
|
||||
await deleteJSON(`/subscriptions/${id}`);
|
||||
|
||||
const retrievePaymentMethod = async(logger, id) => await getJSON(`/payment_methods/${id}`);
|
||||
|
||||
const calculateInvoiceAmount = async(logger, products) => {
|
||||
assert.ok(Array.isArray(products) && products.length, 'calculateInvoiceAmount: products must be array');
|
||||
assert.ok(!products.find((p) => !p.priceId || !p.quantity), 'calculateInvoiceAmount: invalid products array');
|
||||
|
||||
const prices = await Promise.all(products.map((p) => {
|
||||
return getJSON(`/prices/${p.priceId}?expand[]=tiers`);
|
||||
}));
|
||||
logger.debug({prices, products}, 'calculateInvoiceAmount retrieved prices');
|
||||
|
||||
const total = prices.reduce((acc, pr) => {
|
||||
const product = products.find((product) => product.priceId === pr.id);
|
||||
logger.debug({product}, 'calculating price for line item');
|
||||
if (pr.billing_scheme === 'per_unit') {
|
||||
const lineItemCost = pr.unit_amount * product.quantity;
|
||||
logger.debug(`per-unit pricing: ${product.quantity} * ${pr.unit_amount} = ${lineItemCost} usd`);
|
||||
return acc + lineItemCost;
|
||||
}
|
||||
else if (pr.billing_scheme === 'tiered') {
|
||||
const tier = pr.tiers.find((t) => product.quantity <= t.up_to || t.up_to === null);
|
||||
if (typeof tier.flat_amount === 'number') {
|
||||
const lineItemCost = tier.flat_amount;
|
||||
logger.debug({tier}, `tiered pricing, flat amount: ${product.quantity} = ${lineItemCost} usd`);
|
||||
return acc + lineItemCost;
|
||||
}
|
||||
else {
|
||||
const lineItemCost = tier.unit_amount * product.quantity;
|
||||
logger.debug({tier},
|
||||
`tiered pricing, per-unit based: ${product.quantity} * ${tier.unit_amount} = ${lineItemCost} usd`);
|
||||
return acc + (tier.unit_amount * product.quantity);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO: handle volume pricing
|
||||
assert(false, `calculateInvoiceAmount: billing_scheme ${pr.billing_scheme} not implemented!!`);
|
||||
}
|
||||
|
||||
}, 0);
|
||||
logger.debug(`calculateInvoiceAmount total cost ${total}`);
|
||||
return {amount: total, currency: prices[0].currency};
|
||||
};
|
||||
|
||||
const createPaymentIntent = async(logger,
|
||||
{account_sid, stripe_customer_id, amount, email, currency, stripe_payment_method_id}) => {
|
||||
const obj = {
|
||||
amount,
|
||||
currency,
|
||||
customer: stripe_customer_id,
|
||||
payment_method: stripe_payment_method_id,
|
||||
receipt_email: email,
|
||||
metadata: {
|
||||
account_sid
|
||||
},
|
||||
setup_future_usage: 'off_session'
|
||||
};
|
||||
const result = await postForm('/payment_intents', formurlencoded(obj));
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listProducts,
|
||||
listPrices,
|
||||
createCustomer,
|
||||
retrieveCustomer,
|
||||
updateCustomer,
|
||||
deleteCustomer,
|
||||
createSubscription,
|
||||
retrieveSubscription,
|
||||
cancelSubscription,
|
||||
updateSubscription,
|
||||
retrievePaymentMethod,
|
||||
calculateInvoiceAmount,
|
||||
createPaymentIntent,
|
||||
attachPaymentMethod,
|
||||
detachPaymentMethod,
|
||||
retrieveUpcomingInvoice,
|
||||
payOutstandingInvoicesForCustomer,
|
||||
retrieveInvoice,
|
||||
retrieveProduct,
|
||||
retrievePricesForProduct
|
||||
};
|
||||
Reference in New Issue
Block a user