Files
jambonz-api-server/lib/models/account.js
Markus Frindt 23cd4408a5 Feat/obscure sensitive bucket credentials (#375)
* obscure sensitive bucket credentials

* npm audit fix

* fix condition

* add test suite encrypt-decrypt.test

* revert docker-compose

* update pipeline

---------

Co-authored-by: mfrindt <m.frindt@cognigy.com>
2025-01-03 07:12:44 -05:00

349 lines
9.9 KiB
JavaScript

const debug = require('debug')('jambonz:api-server');
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const { v4: uuid } = require('uuid');
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
const retrieveSql = `SELECT * from accounts acc
LEFT JOIN webhooks AS rh
ON acc.registration_hook_sid = rh.webhook_sid
LEFT JOIN webhooks AS qh
ON acc.queue_event_hook_sid = qh.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 = ?, stripe_payment_method_id=?, 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`;
const extractBucketCredential = (obj) => {
try {
const {bucket_credential} = obj;
if (bucket_credential) {
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
}
} catch (error) {
console.error('Error while decrypting data', error);
}
};
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.acc;
/* registration hook */
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
Object.assign(obj, {registration_hook: row.rh});
delete obj.registration_hook.webhook_sid;
}
else obj.registration_hook = null;
delete obj.registration_hook_sid;
/* queue event hook */
if (row.qh && Object.keys(row.qh).length && row.qh.url !== null) {
Object.assign(obj, {queue_event_hook: row.qh});
delete obj.queue_event_hook.webhook_sid;
}
else obj.queue_event_hook = null;
delete obj.queue_event_hook_sid;
extractBucketCredential(obj);
return obj;
});
}
class Account extends Model {
constructor() {
super();
}
/**
* list all accounts
*/
static retrieveAll(service_provider_sid, account_sid) {
let sql = retrieveSql;
const args = [];
if (account_sid) {
sql = `${sql} WHERE acc.account_sid = ?`;
args.push(account_sid);
}
else if (service_provider_sid) {
sql = `${sql} WHERE acc.service_provider_sid = ?`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
}
/**
* retrieve an account
*/
static retrieve(sid, service_provider_sid) {
const args = [sid];
let sql = `${retrieveSql} WHERE acc.account_sid = ?`;
if (service_provider_sid) {
sql = `${retrieveSql} WHERE acc.account_sid = ? AND acc.service_provider_sid = ?`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
}
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 */
/* update account.is_active to 1, if account is deactivated */
await promisePool.execute(
'UPDATE accounts SET plan_type = \'paid\', is_active = 1 WHERE account_sid = ?',
[account_sid]);
return true;
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {id, card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, id, 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';
Account.fields = [
{
name: 'account_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'service_provider_sid',
type: 'string',
required: true
},
{
name: 'sip_realm',
type: 'string',
},
{
name: 'queue_event_hook_sid',
type: 'string',
},
{
name: 'registration_hook_sid',
type: 'string',
},
{
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',
},
{
name: 'subspace_client_id',
type: 'string',
},
{
name: 'subspace_client_secret',
type: 'string',
},
{
name: 'subspace_sip_teleport_id',
type: 'string',
},
{
name: 'subspace_sip_teleport_destinations',
type: 'string',
},
{
name: 'siprec_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number'
},
{
name: 'record_format',
type: 'string'
},
{
name: 'bucket_credential',
type: 'string'
}
];
module.exports = Account;