mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2025-12-18 21:37:43 +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:
@@ -11,7 +11,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 14
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- run: npm test
|
||||
56
app.js
56
app.js
@@ -11,7 +11,6 @@ const express = require('express');
|
||||
const app = express();
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const authStrategy = require('./lib/auth')(logger);
|
||||
const routes = require('./lib/routes');
|
||||
|
||||
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||
@@ -19,13 +18,19 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||
process.env.JAMBONES_MYSQL_PASSWORD &&
|
||||
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
|
||||
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
|
||||
const {queryCdrs, queryAlerts, writeCdrs, writeAlerts, AlertType} = require('@jambonz/time-series')(
|
||||
logger, process.env.JAMBONES_TIME_SERIES_HOST
|
||||
);
|
||||
const {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
purgeCalls,
|
||||
retrieveSet
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
@@ -34,7 +39,10 @@ const {
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
lookupAppByPhoneNumber
|
||||
lookupAppByPhoneNumber,
|
||||
lookupCarrierBySid,
|
||||
lookupSipGatewayBySid,
|
||||
lookupSmppGatewayBySid
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
@@ -44,22 +52,35 @@ const {
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
}, logger);
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const authStrategy = require('./lib/auth')(logger, retrieveKey);
|
||||
|
||||
passport.use(authStrategy);
|
||||
|
||||
app.locals = app.locals || {};
|
||||
Object.assign(app.locals, {
|
||||
app.locals = {
|
||||
...app.locals,
|
||||
logger,
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
lookupAppByPhoneNumber
|
||||
});
|
||||
lookupAppByPhoneNumber,
|
||||
lookupCarrierBySid,
|
||||
lookupSipGatewayBySid,
|
||||
lookupSmppGatewayBySid,
|
||||
queryCdrs,
|
||||
queryAlerts,
|
||||
writeCdrs,
|
||||
writeAlerts,
|
||||
AlertType
|
||||
};
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
return (req, res, next) => {
|
||||
@@ -69,13 +90,20 @@ const unless = (paths, middleware) => {
|
||||
};
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use('/v1', unless(['/login', '/Users', '/messaging', '/outboundSMS'], passport.authenticate('bearer', {
|
||||
session: false
|
||||
})));
|
||||
app.use(express.urlencoded({extended: true}));
|
||||
app.use(unless(['/stripe'], express.json()));
|
||||
app.use('/v1', unless(
|
||||
[
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/signin',
|
||||
'/login',
|
||||
'/messaging',
|
||||
'/outboundSMS',
|
||||
'/AccountTest',
|
||||
'/InviteCodes',
|
||||
'/PredefinedCarriers'
|
||||
], passport.authenticate('bearer', {session: false})));
|
||||
app.use('/', routes);
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err, 'burped error');
|
||||
|
||||
BIN
data/test_audio.wav
Normal file
BIN
data/test_audio.wav
Normal file
Binary file not shown.
52
db/add-predefined-carriers.sql
Normal file
52
db/add-predefined-carriers.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
-- create predefined carriers
|
||||
insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus,
|
||||
requires_register, register_username, register_password,
|
||||
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
|
||||
VALUES
|
||||
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
|
||||
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
|
||||
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
|
||||
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- TelecomXchange gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
|
||||
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
|
||||
|
||||
-- twilio gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0),
|
||||
('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0),
|
||||
('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0),
|
||||
('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0),
|
||||
('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0),
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
|
||||
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
|
||||
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
|
||||
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
|
||||
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
|
||||
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
|
||||
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
|
||||
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
|
||||
@@ -1,4 +1,4 @@
|
||||
insert into service_providers (service_provider_sid, name)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider');
|
||||
insert into accounts (account_sid, service_provider_sid, name)
|
||||
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account');
|
||||
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
|
||||
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'foobar');
|
||||
|
||||
@@ -2,20 +2,46 @@
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
|
||||
DROP TABLE IF EXISTS account_products;
|
||||
|
||||
DROP TABLE IF EXISTS account_subscriptions;
|
||||
|
||||
DROP TABLE IF EXISTS beta_invite_codes;
|
||||
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
|
||||
DROP TABLE IF EXISTS api_keys;
|
||||
DROP TABLE IF EXISTS predefined_sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||
DROP TABLE IF EXISTS predefined_carriers;
|
||||
|
||||
DROP TABLE IF EXISTS account_offers;
|
||||
|
||||
DROP TABLE IF EXISTS products;
|
||||
|
||||
DROP TABLE IF EXISTS api_keys;
|
||||
|
||||
DROP TABLE IF EXISTS sbc_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||
|
||||
DROP TABLE IF EXISTS signup_history;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS speech_credentials;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS phone_numbers;
|
||||
|
||||
DROP TABLE IF EXISTS sip_gateways;
|
||||
@@ -30,6 +56,41 @@ DROP TABLE IF EXISTS service_providers;
|
||||
|
||||
DROP TABLE IF EXISTS webhooks;
|
||||
|
||||
CREATE TABLE account_static_ips
|
||||
(
|
||||
account_static_ip_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
public_ipv4 VARCHAR(16) NOT NULL UNIQUE ,
|
||||
private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
|
||||
PRIMARY KEY (account_static_ip_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE account_subscriptions
|
||||
(
|
||||
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
pending BOOLEAN NOT NULL DEFAULT false,
|
||||
effective_start_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
effective_end_date DATETIME,
|
||||
change_reason VARCHAR(255),
|
||||
stripe_subscription_id VARCHAR(56),
|
||||
stripe_payment_method_id VARCHAR(56),
|
||||
stripe_statement_descriptor VARCHAR(255),
|
||||
last4 VARCHAR(512),
|
||||
exp_month INTEGER,
|
||||
exp_year INTEGER,
|
||||
card_type VARCHAR(16),
|
||||
pending_reason VARBINARY(52),
|
||||
PRIMARY KEY (account_subscription_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE beta_invite_codes
|
||||
(
|
||||
invite_code CHAR(6) NOT NULL UNIQUE ,
|
||||
in_use BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (invite_code)
|
||||
);
|
||||
|
||||
CREATE TABLE call_routes
|
||||
(
|
||||
call_route_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -40,6 +101,15 @@ application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE dns_records
|
||||
(
|
||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
record_type VARCHAR(6) NOT NULL,
|
||||
record_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (dns_record_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
lcr_route_sid CHAR(36),
|
||||
@@ -49,6 +119,61 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) COMMENT='Least cost routing table';
|
||||
|
||||
CREATE TABLE predefined_carriers
|
||||
(
|
||||
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
requires_static_ip BOOLEAN NOT NULL DEFAULT false,
|
||||
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
|
||||
requires_register BOOLEAN NOT NULL DEFAULT false,
|
||||
register_username VARCHAR(64),
|
||||
register_sip_realm VARCHAR(64),
|
||||
register_password VARCHAR(64),
|
||||
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
|
||||
inbound_auth_username VARCHAR(64),
|
||||
inbound_auth_password VARCHAR(64),
|
||||
diversion VARCHAR(32),
|
||||
PRIMARY KEY (predefined_carrier_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE predefined_sip_gateways
|
||||
(
|
||||
predefined_sip_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
|
||||
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
predefined_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (predefined_sip_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE products
|
||||
(
|
||||
product_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
|
||||
PRIMARY KEY (product_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE account_products
|
||||
(
|
||||
account_product_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_subscription_sid CHAR(36) NOT NULL,
|
||||
product_sid CHAR(36) NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
PRIMARY KEY (account_product_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE account_offers
|
||||
(
|
||||
account_offer_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
product_sid CHAR(36) NOT NULL,
|
||||
stripe_product_id VARCHAR(56) NOT NULL,
|
||||
PRIMARY KEY (account_offer_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE api_keys
|
||||
(
|
||||
api_key_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -61,6 +186,15 @@ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (api_key_sid)
|
||||
) COMMENT='An authorization token that is used to access the REST api';
|
||||
|
||||
CREATE TABLE sbc_addresses
|
||||
(
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
service_provider_sid CHAR(36),
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE ms_teams_tenants
|
||||
(
|
||||
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -71,67 +205,121 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
|
||||
PRIMARY KEY (ms_teams_tenant_sid)
|
||||
) COMMENT='A Microsoft Teams customer tenant';
|
||||
|
||||
CREATE TABLE sbc_addresses
|
||||
CREATE TABLE signup_history
|
||||
(
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255),
|
||||
signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (email)
|
||||
);
|
||||
|
||||
CREATE TABLE smpp_addresses
|
||||
(
|
||||
smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
use_tls BOOLEAN NOT NULL DEFAULT 0,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT 1,
|
||||
service_provider_sid CHAR(36),
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
PRIMARY KEY (smpp_address_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE speech_credentials
|
||||
(
|
||||
speech_credential_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
service_provider_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
vendor VARCHAR(32) NOT NULL,
|
||||
credential VARCHAR(8192) NOT NULL,
|
||||
use_for_tts BOOLEAN DEFAULT true,
|
||||
use_for_stt BOOLEAN DEFAULT true,
|
||||
last_used DATETIME,
|
||||
last_tested DATETIME,
|
||||
tts_tested_ok BOOLEAN,
|
||||
stt_tested_ok BOOLEAN,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (speech_credential_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name CHAR(36) NOT NULL UNIQUE ,
|
||||
hashed_password VARCHAR(1024) NOT NULL,
|
||||
salt CHAR(16) NOT NULL,
|
||||
force_change BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
pending_email VARCHAR(255),
|
||||
phone VARCHAR(20) UNIQUE ,
|
||||
hashed_password VARCHAR(1024),
|
||||
account_sid CHAR(36),
|
||||
service_provider_sid CHAR(36),
|
||||
force_change BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provider VARCHAR(255) NOT NULL,
|
||||
provider_userid VARCHAR(255),
|
||||
scope VARCHAR(16) NOT NULL DEFAULT 'read-write',
|
||||
phone_activation_code VARCHAR(16),
|
||||
email_activation_code VARCHAR(16),
|
||||
email_validated BOOLEAN NOT NULL DEFAULT false,
|
||||
phone_validated BOOLEAN NOT NULL DEFAULT false,
|
||||
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (user_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE voip_carriers
|
||||
(
|
||||
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
|
||||
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a sip trunk that is associated with a specific account',
|
||||
service_provider_sid CHAR(36),
|
||||
application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
|
||||
e164_leading_plus BOOLEAN NOT NULL DEFAULT false,
|
||||
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
|
||||
requires_register BOOLEAN NOT NULL DEFAULT false,
|
||||
register_username VARCHAR(64),
|
||||
register_sip_realm VARCHAR(64),
|
||||
register_password VARCHAR(64),
|
||||
tech_prefix VARCHAR(16),
|
||||
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
|
||||
inbound_auth_username VARCHAR(64),
|
||||
inbound_auth_password VARCHAR(64),
|
||||
diversion VARCHAR(32),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
smpp_system_id VARCHAR(255),
|
||||
smpp_password VARCHAR(64),
|
||||
smpp_enquire_link_interval INTEGER DEFAULT 0,
|
||||
smpp_inbound_system_id VARCHAR(255),
|
||||
smpp_inbound_password VARCHAR(64),
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
CREATE TABLE smpp_gateways
|
||||
(
|
||||
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(128) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 2775,
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT 1,
|
||||
inbound BOOLEAN NOT NULL DEFAULT 0 COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL DEFAULT 1 COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
use_tls BOOLEAN DEFAULT 0,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (smpp_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(32) NOT NULL UNIQUE ,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
service_provider_sid CHAR(36) COMMENT 'if not null, this number is a test number for the associated service provider',
|
||||
PRIMARY KEY (phone_number_sid)
|
||||
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
|
||||
|
||||
CREATE TABLE webhooks
|
||||
(
|
||||
webhook_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
url VARCHAR(1024) NOT NULL,
|
||||
method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
|
||||
username VARCHAR(255),
|
||||
password VARCHAR(255),
|
||||
PRIMARY KEY (webhook_sid)
|
||||
) COMMENT='An HTTP callback';
|
||||
) COMMENT='A phone number that has been assigned to an account';
|
||||
|
||||
CREATE TABLE sip_gateways
|
||||
(
|
||||
sip_gateway_sid CHAR(36),
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
@@ -150,11 +338,22 @@ priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempt
|
||||
PRIMARY KEY (lcr_carrier_set_entry_sid)
|
||||
) COMMENT='An entry in the LCR routing list';
|
||||
|
||||
CREATE TABLE webhooks
|
||||
(
|
||||
webhook_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
url VARCHAR(1024) NOT NULL,
|
||||
method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
|
||||
username VARCHAR(255),
|
||||
password VARCHAR(255),
|
||||
PRIMARY KEY (webhook_sid)
|
||||
) COMMENT='An HTTP callback';
|
||||
|
||||
CREATE TABLE applications
|
||||
(
|
||||
application_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
|
||||
service_provider_sid CHAR(36) COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider',
|
||||
account_sid CHAR(36) COMMENT 'account that this application belongs to (if null, this is a service provider test application)',
|
||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
||||
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
@@ -163,6 +362,7 @@ speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (application_sid)
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
@@ -184,69 +384,144 @@ name VARCHAR(64) NOT NULL,
|
||||
sip_realm VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account',
|
||||
service_provider_sid CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
|
||||
registration_hook_sid CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register',
|
||||
queue_event_hook_sid CHAR(36),
|
||||
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
webhook_secret VARCHAR(36),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
plan_type ENUM('trial','free','paid') NOT NULL DEFAULT 'trial',
|
||||
stripe_customer_id VARCHAR(56),
|
||||
webhook_secret VARCHAR(36) NOT NULL,
|
||||
disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
|
||||
trial_end_date DATETIME,
|
||||
deactivated_reason VARCHAR(255),
|
||||
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_sid);
|
||||
CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
|
||||
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
|
||||
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
|
||||
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
|
||||
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
||||
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
|
||||
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
|
||||
|
||||
CREATE INDEX product_sid_idx ON products (product_sid);
|
||||
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
|
||||
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
|
||||
ALTER TABLE account_products ADD FOREIGN KEY account_subscription_sid_idxfk (account_subscription_sid) REFERENCES account_subscriptions (account_subscription_sid);
|
||||
|
||||
ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REFERENCES products (product_sid);
|
||||
|
||||
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
|
||||
CREATE INDEX account_sid_idx ON account_offers (account_sid);
|
||||
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX product_sid_idx ON account_offers (product_sid);
|
||||
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
|
||||
|
||||
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
|
||||
CREATE INDEX account_sid_idx ON api_keys (account_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
|
||||
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
|
||||
|
||||
CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON sbc_addresses (service_provider_sid);
|
||||
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
|
||||
CREATE INDEX email_idx ON signup_history (email);
|
||||
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
|
||||
|
||||
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||
CREATE INDEX name_idx ON users (name);
|
||||
CREATE INDEX email_idx ON users (email);
|
||||
CREATE INDEX phone_idx ON users (phone);
|
||||
CREATE INDEX account_sid_idx ON users (account_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
|
||||
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
|
||||
CREATE INDEX name_idx ON voip_carriers (name);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
||||
CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
|
||||
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX number_idx ON phone_numbers (number);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
|
||||
CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
|
||||
|
||||
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
|
||||
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
|
||||
|
||||
CREATE INDEX application_sid_idx ON applications (application_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON applications (account_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
@@ -262,10 +537,12 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
|
||||
CREATE INDEX account_sid_idx ON accounts (account_sid);
|
||||
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
|
||||
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
|
||||
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
1309
db/jambones.sqs
1309
db/jambones.sqs
File diff suppressed because one or more lines are too long
@@ -1,71 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
const {getMysqlConnection} = require('../lib/db');
|
||||
const crypto = require('crypto');
|
||||
console.log('reset_admin_password');
|
||||
const {promisePool} = require('../lib/db');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
const sqlInsert = `INSERT into users
|
||||
(user_sid, name, hashed_password, salt)
|
||||
values (?, ?, ?, ?)
|
||||
(user_sid, name, email, hashed_password, force_change, provider, email_validated)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const sqlChangeAdminToken = `UPDATE api_keys set token = ?
|
||||
WHERE account_sid IS NULL
|
||||
AND service_provider_sid IS NULL`;
|
||||
const sqlQueryAccount = 'SELECT * from accounts LIMIT 1';
|
||||
const sqlAddAccountAdminToken = `INSERT into api_keys (api_key_sid, token, account_sid)
|
||||
VALUES (?, ?, ?)`;
|
||||
|
||||
/**
|
||||
* generates random string of characters i.e salt
|
||||
* @function
|
||||
* @param {number} length - Length of the random string.
|
||||
*/
|
||||
const genRandomString = (len) => {
|
||||
return crypto.randomBytes(Math.ceil(len / 2))
|
||||
.toString('hex') /** convert to hexadecimal format */
|
||||
.slice(0, len); /** return required number of characters */
|
||||
};
|
||||
|
||||
/**
|
||||
* hash password with sha512.
|
||||
* @function
|
||||
* @param {string} password - List of required fields.
|
||||
* @param {string} salt - Data to be validated.
|
||||
*/
|
||||
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 saltHashPassword = (userpassword) => {
|
||||
var salt = genRandomString(16); /** Gives us salt of length 16 */
|
||||
return sha512(userpassword, salt);
|
||||
};
|
||||
|
||||
/* reset admin password */
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return console.log(err, 'Error connecting to database');
|
||||
|
||||
/* delete admin user if it exists */
|
||||
conn.query('DELETE from users where name = "admin"', (err) => {
|
||||
if (err) return console.log(err, 'Error removing admin user');
|
||||
const {salt, passwordHash} = saltHashPassword('admin');
|
||||
const sid = uuidv4();
|
||||
conn.query(sqlInsert, [
|
||||
const doIt = async() => {
|
||||
const passwordHash = await generateHashedPassword('admin');
|
||||
const sid = uuidv4();
|
||||
await promisePool.execute('DELETE from users where name = "admin"');
|
||||
await promisePool.execute(sqlInsert,
|
||||
[
|
||||
sid,
|
||||
'admin',
|
||||
'joe@foo.bar',
|
||||
passwordHash,
|
||||
salt
|
||||
], (err) => {
|
||||
if (err) return console.log(err, 'Error inserting admin user');
|
||||
console.log('successfully reset admin password');
|
||||
const uuid = uuidv4();
|
||||
conn.query(sqlChangeAdminToken, [uuid], (err) => {
|
||||
if (err) return console.log(err, 'Error updating admin token');
|
||||
console.log('successfully changed admin tokens');
|
||||
conn.release();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1,
|
||||
'local',
|
||||
1
|
||||
]
|
||||
);
|
||||
|
||||
/* reset admin token */
|
||||
const uuid = uuidv4();
|
||||
await promisePool.query(sqlChangeAdminToken, [uuid]);
|
||||
|
||||
/* create admin token for single account */
|
||||
const api_key_sid = uuidv4();
|
||||
const token = uuidv4();
|
||||
const [r] = await promisePool.query(sqlQueryAccount);
|
||||
await promisePool.execute(sqlAddAccountAdminToken, [api_key_sid, token, r[0].account_sid]);
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
doIt();
|
||||
|
||||
102
db/seed-integration-test.sql
Normal file
102
db/seed-integration-test.sql
Normal file
@@ -0,0 +1,102 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
insert into sbc_addresses (sbc_address_sid, ipv4, port)
|
||||
values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060);
|
||||
insert into sbc_addresses (sbc_address_sid, ipv4, port)
|
||||
values('de5ed2f1-bccd-4600-a95e-cef46e9a3a4f', '3.34.102.122', 5060);
|
||||
insert into smpp_addresses (smpp_address_sid, ipv4, port, use_tls, is_primary)
|
||||
values('de5ed2f1-bccd-4600-a95e-cef46e9a3a4f', '34.197.99.29', 2775, 0, 1);
|
||||
insert into smpp_addresses (smpp_address_sid, ipv4, port, use_tls, is_primary)
|
||||
values('049078a0', '3.209.58.102', 3550, 1, 1);
|
||||
|
||||
-- create one service provider and account
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
|
||||
-- create one service provider and one account
|
||||
insert into service_providers (service_provider_sid, name, root_domain)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
|
||||
|
||||
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
|
||||
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
|
||||
|
||||
-- create two applications
|
||||
insert into webhooks(webhook_sid, url, method)
|
||||
values
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
|
||||
|
||||
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
VALUES
|
||||
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),
|
||||
('4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'dial time', '81844b05-714d-4295-8bf3-3b0640a4bf02', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US');
|
||||
|
||||
-- create our products
|
||||
insert into products (product_sid, name, category)
|
||||
values
|
||||
('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'),
|
||||
('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'),
|
||||
('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate');
|
||||
|
||||
-- create predefined carriers
|
||||
insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus,
|
||||
requires_register, register_username, register_password,
|
||||
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
|
||||
VALUES
|
||||
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
|
||||
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
|
||||
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'Peerless Network (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
|
||||
('bdf70650-5328-47aa-b3d0-47cb219d9c6e', '382 Communications (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
|
||||
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- twilio gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0),
|
||||
('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0),
|
||||
('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0),
|
||||
('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0),
|
||||
('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0),
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
|
||||
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
|
||||
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
|
||||
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
|
||||
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
|
||||
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
|
||||
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
|
||||
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
101
db/seed-production-database-open-source.sql
Normal file
101
db/seed-production-database-open-source.sql
Normal file
@@ -0,0 +1,101 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
-- create one service provider and account
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
|
||||
-- create one service provider and one account
|
||||
insert into service_providers (service_provider_sid, name)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider');
|
||||
|
||||
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
|
||||
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
|
||||
|
||||
insert into api_keys (api_key_sid, token, account_sid)
|
||||
values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb597c2a', '9351f46a-678c-43f5-b8a6-d4eb58d131af');
|
||||
|
||||
-- create two applications
|
||||
insert into webhooks(webhook_sid, url, method)
|
||||
values
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
|
||||
|
||||
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
VALUES
|
||||
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),
|
||||
('4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'dial time', '81844b05-714d-4295-8bf3-3b0640a4bf02', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US');
|
||||
|
||||
-- create our products
|
||||
insert into products (product_sid, name, category)
|
||||
values
|
||||
('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'),
|
||||
('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'),
|
||||
('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate');
|
||||
|
||||
-- create predefined carriers
|
||||
insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus,
|
||||
requires_register, register_username, register_password,
|
||||
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
|
||||
VALUES
|
||||
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
|
||||
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
|
||||
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
|
||||
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- TelecomXchange gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
|
||||
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
|
||||
|
||||
-- twilio gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0),
|
||||
('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0),
|
||||
('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0),
|
||||
('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0),
|
||||
('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0),
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
|
||||
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
|
||||
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
|
||||
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
|
||||
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
|
||||
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
|
||||
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
|
||||
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
78
db/seed-production-database.sql
Normal file
78
db/seed-production-database.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- create one service provider
|
||||
insert into service_providers (service_provider_sid, name, description, root_domain)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.us', 'jambonz.us service provider', 'sip.jambonz.us');
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
|
||||
-- create our products
|
||||
insert into products (product_sid, name, category)
|
||||
values
|
||||
('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'),
|
||||
('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'),
|
||||
('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate');
|
||||
|
||||
-- create predefined carriers
|
||||
insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus,
|
||||
requires_register, register_username, register_password,
|
||||
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
|
||||
VALUES
|
||||
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
|
||||
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
|
||||
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
|
||||
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- TelecomXchange gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
|
||||
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
|
||||
|
||||
-- twilio gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0),
|
||||
('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0),
|
||||
('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0),
|
||||
('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0),
|
||||
('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0),
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0),
|
||||
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0),
|
||||
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0),
|
||||
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0),
|
||||
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0),
|
||||
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0),
|
||||
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
|
||||
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
|
||||
|
||||
105
db/webapp-tests.sql
Normal file
105
db/webapp-tests.sql
Normal file
@@ -0,0 +1,105 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
-- create one service provider
|
||||
insert into service_providers (service_provider_sid, name, description, root_domain)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com');
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
|
||||
-- one sbc
|
||||
insert into sbc_addresses (sbc_address_sid, service_provider_sid, ipv4, port) values ('8d6d0fda-4550-41ab-8e2f-60761d81fe7d', null, '3.39.45.30', '5060');
|
||||
|
||||
-- two smpp server
|
||||
insert into smpp_addresses (smpp_address_sid, service_provider_sid, ipv4, port, use_tls, is_primary) values ('e5e8345b-d533-4c29-940b-57aaccc59f8b', null, '3.39.45.30', '2775', false, true);
|
||||
insert into smpp_addresses (smpp_address_sid, service_provider_sid, ipv4, port, use_tls, is_primary) values ('ae060ef3-d5a4-4842-b331-426ec9329fbe', null, '3.39.45.30', '3550', true, true);
|
||||
|
||||
-- one voip carrier with one gateway
|
||||
insert into voip_carriers (voip_carrier_sid, name) values ('5145b436-2f38-4029-8d4c-fd8c67831c7a', 'my test carrier');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound, is_active)
|
||||
values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1);
|
||||
|
||||
-- create the test application and test phone number
|
||||
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST');
|
||||
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST');
|
||||
insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid,
|
||||
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f',
|
||||
'd9c205c6-a129-443e-a9c0-d1bb437d4bb7','6ac36aeb-6bd0-428a-80a1-aed95640a296','google', 'en-US', 'en-US-Standard-C', 'google', 'en-US');
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, service_provider_sid)
|
||||
values ('ec028c46-1363-4b3f-81db-ee33f179d6ba', '18005551212', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '2708b1b3-2736-40ea-b502-c53d8396247f');
|
||||
|
||||
-- create our products
|
||||
insert into products (product_sid, name, category)
|
||||
values
|
||||
('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'),
|
||||
('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'),
|
||||
('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate');
|
||||
|
||||
-- create predefined carriers
|
||||
insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus,
|
||||
requires_register, register_username, register_password,
|
||||
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
|
||||
VALUES
|
||||
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
|
||||
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
|
||||
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'Peerless Network (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
|
||||
('bdf70650-5328-47aa-b3d0-47cb219d9c6e', '382 Communications (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
|
||||
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- twilio gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0),
|
||||
('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0),
|
||||
('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0),
|
||||
('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0),
|
||||
('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '149.91.14.0', 24, 5060, 1, 0),
|
||||
('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '154.51.137.96', 27, 5060, 1, 0),
|
||||
('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '78.40.245.160', 27, 5060, 1, 0),
|
||||
('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.24', 29, 5060, 1, 0),
|
||||
('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.28', 28, 5060, 1, 0),
|
||||
('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.48', 28, 5060, 1, 0),
|
||||
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.242', 32, 5060, 1, 0),
|
||||
('279807e7-649e-41dd-931a-00c460bcc0a2', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.80', 28, 5060, 1, 0),
|
||||
('f5fd66f7-97f6-4979-ab19-eea1557bc872', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.0', 26, 5060, 1, 0),
|
||||
('efcfefdc-451c-4e59-960a-f9e4952d964f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.0', 26, 5060, 1, 0),
|
||||
('b6ae6240-55ac-4c11-892f-a71b2155ea60', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.0', 26, 5060, 1, 0),
|
||||
('5a976337-164b-408e-8748-d8bfb4bd5d76', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.0', 26, 5060, 1, 0),
|
||||
('ed0434ca-7f26-4624-9523-0419d0d2924d', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.0', 26, 5060, 1, 0),
|
||||
('d1a594c2-c14f-4ead-b621-96129bc87886', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.224.0', 24, 5060, 1, 0),
|
||||
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
|
||||
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const getMysqlConnection = require('./mysql');
|
||||
const promisePool = require('./pool');
|
||||
|
||||
module.exports = {
|
||||
getMysqlConnection
|
||||
getMysqlConnection,
|
||||
promisePool
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
lib/db/pool.js
Normal file
10
lib/db/pool.js
Normal file
@@ -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',
|
||||
|
||||
63
lib/models/predefined-carrier.js
Normal file
63
lib/models/predefined-carrier.js
Normal file
@@ -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;
|
||||
28
lib/models/product.js
Normal file
28
lib/models/product.js
Normal file
@@ -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'
|
||||
|
||||
55
lib/models/smpp.js
Normal file
55
lib/models/smpp.js
Normal file
@@ -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;
|
||||
60
lib/models/smpp_gateway.js
Normal file
60
lib/models/smpp_gateway.js
Normal file
@@ -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;
|
||||
92
lib/models/speech-credential.js
Normal file
92
lib/models/speech-credential.js
Normal file
@@ -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;
|
||||
|
||||
57
lib/routes/api/account-test.js
Normal file
57
lib/routes/api/account-test.js
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
117
lib/routes/api/activation-code.js
Normal file
117
lib/routes/api/activation-code.js
Normal file
@@ -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;
|
||||
71
lib/routes/api/add-from-predefined-carrier.js
Normal file
71
lib/routes/api/add-from-predefined-carrier.js
Normal file
@@ -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;
|
||||
35
lib/routes/api/alerts.js
Normal file
35
lib/routes/api/alerts.js
Normal file
@@ -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');
|
||||
|
||||
31
lib/routes/api/availability.js
Normal file
31
lib/routes/api/availability.js
Normal file
@@ -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;
|
||||
32
lib/routes/api/beta-invite-codes.js
Normal file
32
lib/routes/api/beta-invite-codes.js
Normal file
@@ -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;
|
||||
47
lib/routes/api/change-password.js
Normal file
47
lib/routes/api/change-password.js
Normal file
@@ -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;
|
||||
46
lib/routes/api/charges.js
Normal file
46
lib/routes/api/charges.js
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
82
lib/routes/api/forgot-password.js
Normal file
82
lib/routes/api/forgot-password.js
Normal file
@@ -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;
|
||||
@@ -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
|
||||
|
||||
|
||||
34
lib/routes/api/invite-codes.js
Normal file
34
lib/routes/api/invite-codes.js
Normal file
@@ -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;
|
||||
26
lib/routes/api/invoices.js
Normal file
26
lib/routes/api/invoices.js
Normal file
@@ -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;
|
||||
@@ -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});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
23
lib/routes/api/logout.js
Normal file
23
lib/routes/api/logout.js
Normal file
@@ -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;
|
||||
|
||||
7
lib/routes/api/predefined-carriers.js
Normal file
7
lib/routes/api/predefined-carriers.js
Normal file
@@ -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;
|
||||
108
lib/routes/api/prices.js
Normal file
108
lib/routes/api/prices.js
Normal file
@@ -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;
|
||||
78
lib/routes/api/products.js
Normal file
78
lib/routes/api/products.js
Normal file
@@ -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;
|
||||
37
lib/routes/api/recent-calls.js
Normal file
37
lib/routes/api/recent-calls.js
Normal file
@@ -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;
|
||||
344
lib/routes/api/register.js
Normal file
344
lib/routes/api/register.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
85
lib/routes/api/signin.js
Normal file
85
lib/routes/api/signin.js
Normal file
@@ -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;
|
||||
|
||||
67
lib/routes/api/sip-realm.js
Normal file
67
lib/routes/api/sip-realm.js
Normal file
@@ -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;
|
||||
53
lib/routes/api/smpp-gateways.js
Normal file
53
lib/routes/api/smpp-gateways.js
Normal file
@@ -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;
|
||||
30
lib/routes/api/smpps.js
Normal file
30
lib/routes/api/smpps.js
Normal file
@@ -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;
|
||||
|
||||
248
lib/routes/api/speech-credentials.js
Normal file
248
lib/routes/api/speech-credentials.js
Normal file
@@ -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;
|
||||
48
lib/routes/api/stripe-customer-id.js
Normal file
48
lib/routes/api/stripe-customer-id.js
Normal file
@@ -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;
|
||||
334
lib/routes/api/subscriptions.js
Normal file
334
lib/routes/api/subscriptions.js
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
178
lib/routes/api/utils.js
Normal file
178
lib/routes/api/utils.js
Normal file
@@ -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;
|
||||
|
||||
21
lib/routes/api/webhooks.js
Normal file
21
lib/routes/api/webhooks.js
Normal file
@@ -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;
|
||||
24
lib/routes/error.js
Normal file
24
lib/routes/error.js
Normal file
@@ -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));
|
||||
|
||||
|
||||
5
lib/routes/stripe/index.js
Normal file
5
lib/routes/stripe/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
|
||||
router.use('/webhook', require('./webhook'));
|
||||
|
||||
module.exports = router;
|
||||
71
lib/routes/stripe/webhook.js
Normal file
71
lib/routes/stripe/webhook.js
Normal file
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
123
lib/utils/dns-utils.js
Normal file
123
lib/utils/dns-utils.js
Normal file
@@ -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
|
||||
};
|
||||
34
lib/utils/email-utils.js
Normal file
34
lib/utils/email-utils.js
Normal file
@@ -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
|
||||
};
|
||||
29
lib/utils/encrypt-decrypt.js
Normal file
29
lib/utils/encrypt-decrypt.js
Normal file
@@ -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
|
||||
};
|
||||
@@ -16,8 +16,15 @@ class DbErrorUnprocessableRequest extends DbError {
|
||||
}
|
||||
}
|
||||
|
||||
class DbErrorForbidden extends DbError {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
};
|
||||
|
||||
22
lib/utils/free_plans.json
Normal file
22
lib/utils/free_plans.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
112
lib/utils/oauth-utils.js
Normal file
112
lib/utils/oauth-utils.js
Normal file
@@ -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
|
||||
};
|
||||
24
lib/utils/password-utils.js
Normal file
24
lib/utils/password-utils.js
Normal file
@@ -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
|
||||
};
|
||||
22
lib/utils/phone-number-utils.js
Normal file
22
lib/utils/phone-number-utils.js
Normal file
@@ -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
|
||||
};
|
||||
60
lib/utils/speech-utils.js
Normal file
60
lib/utils/speech-utils.js
Normal file
@@ -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
|
||||
};
|
||||
224
lib/utils/stripe-utils.js
Normal file
224
lib/utils/stripe-utils.js
Normal file
@@ -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
|
||||
};
|
||||
33
package.json
33
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_MYSQL_PORT=3360 JAMBONES_REDIS_HOST=localhost JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ | ./node_modules/.bin/tap-spec",
|
||||
"test": "NODE_ENV=test JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ",
|
||||
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
@@ -16,29 +16,40 @@
|
||||
"url": "https://github.com/jambonz/jambonz-api-server.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.5.5",
|
||||
"@jambonz/messaging-382com": "0.0.2",
|
||||
"@jambonz/messaging-peerless": "0.0.9",
|
||||
"@jambonz/messaging-simwood": "0.0.4",
|
||||
"@jambonz/realtimedb-helpers": "0.2.19",
|
||||
"@google-cloud/speech": "^4.2.0",
|
||||
"@google-cloud/text-to-speech": "^3.1.3",
|
||||
"@jambonz/db-helpers": "^0.6.12",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.3",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"argon2-ffi": "^2.0.0",
|
||||
"aws-sdk": "^2.839.0",
|
||||
"bent": "^7.3.12",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.17.1",
|
||||
"form-data": "^2.3.3",
|
||||
"form-urlencoded": "^4.2.1",
|
||||
"google-libphonenumber": "^3.2.15",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mailgun.js": "^3.3.0",
|
||||
"mysql2": "^2.2.5",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pino": "^5.17.0",
|
||||
"qs": "^6.7.0",
|
||||
"request": "^2.88.2",
|
||||
"request-debug": "^0.2.0",
|
||||
"swagger-ui-express": "^4.1.5",
|
||||
"short-uuid": "^4.1.0",
|
||||
"stripe": "^8.138.0",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"uuid": "^3.4.0",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"blue-tape": "^1.0.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"tap-spec": "^5.0.0"
|
||||
"tape": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -16,6 +16,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
|
||||
test('account tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const logger = app.locals.logger;
|
||||
let sid;
|
||||
try {
|
||||
let result;
|
||||
@@ -25,6 +26,61 @@ test('account tests', async(t) => {
|
||||
const service_provider_sid = await createServiceProvider(request);
|
||||
const phone_number_sid = await createPhoneNumber(request, voip_carrier_sid);
|
||||
|
||||
/* add invite codes */
|
||||
result = await request.post('/BetaInviteCodes', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
count: 2
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && 2 === parseInt(result.body.added), 'successfully added 2 beta codes');
|
||||
//console.log(result.body.codes);
|
||||
|
||||
/* claim an invite code */
|
||||
/*
|
||||
const mycodes = result.body.codes;
|
||||
result = await request.post('/InviteCodes', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
test: true,
|
||||
code: mycodes[0]
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully tested a beta codes');
|
||||
result = await request.post('/InviteCodes', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
code: mycodes[0]
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully claimed a beta codes');
|
||||
*/
|
||||
|
||||
result = await request.post('/BetaInviteCodes', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
count: 50
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && 50 === parseInt(result.body.added), 'successfully added 50 beta codes');
|
||||
|
||||
result = await request.post('/BetaInviteCodes', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && 1 === parseInt(result.body.added), 'successfully added 1 beta codes');
|
||||
|
||||
/* add an account */
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -36,12 +92,22 @@ test('account tests', async(t) => {
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
}
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created account');
|
||||
const sid = result.body.sid;
|
||||
|
||||
/* query accounts for service providers */
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/Accounts`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
//console.log(result.body);
|
||||
t.ok(result.statusCode === 200, 'successfully queried accounts for service provider');
|
||||
|
||||
/* add an account level api key */
|
||||
result = await request.post(`/ApiKeys`, {
|
||||
auth: authAdmin,
|
||||
@@ -118,23 +184,12 @@ test('account tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully assigned phone number to account');
|
||||
|
||||
/* cannot delete account that has phone numbers assigned */
|
||||
result = await request.delete(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true
|
||||
});
|
||||
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account with phone numbers', 'cannot delete account with phone numbers');
|
||||
|
||||
/* delete account */
|
||||
await request.delete(`ApiKeys/${apiKeySid}`, {auth: {bearer: accountLevelToken}});
|
||||
await request.delete(`/PhoneNumbers/${phone_number_sid}`, {auth: authAdmin});
|
||||
result = await request.delete(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted account after removing phone number');
|
||||
t.ok(result.statusCode === 204, 'successfully deleted account');
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
|
||||
36
test/auth.js
36
test/auth.js
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -56,6 +56,7 @@ test('authentication tests', async(t) => {
|
||||
json: true,
|
||||
body: {
|
||||
name: 'accountA1',
|
||||
webhook_secret: 'foobar',
|
||||
registration_hook: {
|
||||
url: 'http://example.com'
|
||||
}
|
||||
@@ -69,6 +70,7 @@ test('authentication tests', async(t) => {
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
webhook_secret: 'foobar',
|
||||
name: 'accountA2'
|
||||
}
|
||||
});
|
||||
@@ -80,6 +82,7 @@ test('authentication tests', async(t) => {
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
webhook_secret: 'foobar',
|
||||
name: 'accountB1'
|
||||
}
|
||||
});
|
||||
@@ -91,6 +94,7 @@ test('authentication tests', async(t) => {
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
webhook_secret: 'foobar',
|
||||
name: 'accountB2'
|
||||
}
|
||||
});
|
||||
@@ -170,6 +174,7 @@ test('authentication tests', async(t) => {
|
||||
json: true,
|
||||
body: {
|
||||
name: 'accountC',
|
||||
webhook_secret: 'foobar',
|
||||
service_provider_sid: spA_sid
|
||||
}
|
||||
});
|
||||
@@ -206,7 +211,7 @@ test('authentication tests', async(t) => {
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
sip_realm: 'sip.foo.bar'
|
||||
name: 'joe knife'
|
||||
}
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
@@ -324,33 +329,6 @@ test('authentication tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 404, 'using account token A1 we are not able to retrieve application A2s');
|
||||
|
||||
|
||||
/* service provider token can not be used to add phone number */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
auth: {bearer: spA_token},
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
number: '16173333456',
|
||||
voip_carrier_sid
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 403, 'service provider token can not be used to add phone number');
|
||||
|
||||
/* account token can not be used to add phone number */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
auth: {bearer: accA1_token},
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true,
|
||||
body: {
|
||||
number: '16173333456',
|
||||
voip_carrier_sid
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 403, 'account level token can not be used to add phone number');
|
||||
|
||||
/* account level token can not create token for another account */
|
||||
result = await request.post('/ApiKeys', {
|
||||
resolveWithFullResponse: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('creating jambones_test database', (t) => {
|
||||
@@ -10,7 +10,7 @@ test('creating jambones_test database', (t) => {
|
||||
});
|
||||
|
||||
test('creating schema', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('schema successfully created');
|
||||
t.end();
|
||||
@@ -18,9 +18,17 @@ test('creating schema', (t) => {
|
||||
});
|
||||
|
||||
test('creating auth token', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('auth token successfully created');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('add predefined carriers', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/add-predefined-carriers.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('predefined carriers added');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
908
test/data/subscription.json
Normal file
908
test/data/subscription.json
Normal file
@@ -0,0 +1,908 @@
|
||||
{
|
||||
"id": "sub_J5d3C56ZGMDZFA",
|
||||
"object": "subscription",
|
||||
"application_fee_percent": null,
|
||||
"billing_cycle_anchor": 1615381795,
|
||||
"billing_thresholds": null,
|
||||
"cancel_at": null,
|
||||
"cancel_at_period_end": false,
|
||||
"canceled_at": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1615381795,
|
||||
"current_period_end": 1618060195,
|
||||
"current_period_start": 1615381795,
|
||||
"customer": "cus_J5coVe5AQ5UR6h",
|
||||
"days_until_due": null,
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"discount": null,
|
||||
"ended_at": null,
|
||||
"items": {
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"id": "si_J5d3M9tUQgrnPa",
|
||||
"object": "subscription_item",
|
||||
"billing_thresholds": null,
|
||||
"created": 1615381796,
|
||||
"metadata": {
|
||||
"product_sid": "c4403cdb-8e75-4b27-9726-7d8315e3216d"
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRinAxTxXxh2fmnZmorCm2",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": null,
|
||||
"amount_decimal": null,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143177,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4au1au9F2Ysa0",
|
||||
"tiers": [{
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 1000,
|
||||
"unit_amount_decimal": "1000",
|
||||
"up_to": 99
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 900,
|
||||
"unit_amount_decimal": "900",
|
||||
"up_to": 250
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 800,
|
||||
"unit_amount_decimal": "800",
|
||||
"up_to": 499
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 700,
|
||||
"unit_amount_decimal": "700",
|
||||
"up_to": 999
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 500,
|
||||
"unit_amount_decimal": "500",
|
||||
"up_to": null
|
||||
}],
|
||||
"tiers_mode": "volume",
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRinAxTxXxh2fmnZmorCm2",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143177,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4au1au9F2Ysa0",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": "volume",
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null
|
||||
},
|
||||
"quantity": 100,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"tax_rates": []
|
||||
}, {
|
||||
"id": "si_J5d3F3SkebrH4S",
|
||||
"object": "subscription_item",
|
||||
"billing_thresholds": null,
|
||||
"created": 1615381796,
|
||||
"metadata": {
|
||||
"product_sid": "2c815913-5c26-4004-b748-183b459329df"
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRhKAxTxXxh2fmhsYLgyLM",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 100,
|
||||
"amount_decimal": "100",
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1615143086,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4at3poNkI5OSA",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRhKAxTxXxh2fmhsYLgyLM",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1615143086,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4at3poNkI5OSA",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": 100,
|
||||
"unit_amount_decimal": "100"
|
||||
},
|
||||
"quantity": 50,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"tax_rates": []
|
||||
}, {
|
||||
"id": "si_J5d3bcURInlK5Y",
|
||||
"object": "subscription_item",
|
||||
"billing_thresholds": null,
|
||||
"created": 1615381796,
|
||||
"metadata": {
|
||||
"product_sid": "35a9fb10-233d-4eb9-aada-78de5814d680"
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": null,
|
||||
"amount_decimal": null,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"tiers": [{
|
||||
"flat_amount": 0,
|
||||
"flat_amount_decimal": "0",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 10
|
||||
}, {
|
||||
"flat_amount": 500,
|
||||
"flat_amount_decimal": "500",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 30
|
||||
}, {
|
||||
"flat_amount": 1000,
|
||||
"flat_amount_decimal": "1000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 60
|
||||
}, {
|
||||
"flat_amount": 2000,
|
||||
"flat_amount_decimal": "2000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 120
|
||||
}, {
|
||||
"flat_amount": 4000,
|
||||
"flat_amount_decimal": "4000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 300
|
||||
}, {
|
||||
"flat_amount": 5000,
|
||||
"flat_amount_decimal": "5000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": null
|
||||
}],
|
||||
"tiers_mode": "volume",
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": "volume",
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null
|
||||
},
|
||||
"quantity": 60,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"tax_rates": []
|
||||
}],
|
||||
"has_more": false,
|
||||
"total_count": 3,
|
||||
"url": "/v1/subscription_items?subscription=sub_J5d3C56ZGMDZFA"
|
||||
},
|
||||
"latest_invoice": {
|
||||
"id": "in_1ITRnUAxTxXxh2fmmch7bUlr",
|
||||
"object": "invoice",
|
||||
"account_country": "US",
|
||||
"account_name": "drachtio.org",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 96000,
|
||||
"amount_paid": 96000,
|
||||
"amount_remaining": 0,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"billing_reason": "subscription_create",
|
||||
"charge": "ch_1ITRnUAxTxXxh2fmvtLpMbNr",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1615381796,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_J5coVe5AQ5UR6h",
|
||||
"customer_address": null,
|
||||
"customer_email": "daveh@drachtio.org",
|
||||
"customer_name": "Dave Horton",
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1GjPVFAxTxXxh2fm/invst_J5d3z2L367YU08uV3ZStblw4QzWzN4b",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_1GjPVFAxTxXxh2fm/invst_J5d3z2L367YU08uV3ZStblw4QzWzN4b/pdf",
|
||||
"last_finalization_error": null,
|
||||
"lines": {
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"id": "il_1ITRnUAxTxXxh2fmApdPmIJQ",
|
||||
"object": "line_item",
|
||||
"amount": 90000,
|
||||
"currency": "usd",
|
||||
"description": "100 session × concurrent call session (Tier 2 at $9.00 / month)",
|
||||
"discount_amounts": [],
|
||||
"discountable": true,
|
||||
"discounts": [],
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"period": {
|
||||
"end": 1618060195,
|
||||
"start": 1615381795
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRinAxTxXxh2fmnZmorCm2",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": null,
|
||||
"amount_decimal": null,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143177,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4au1au9F2Ysa0",
|
||||
"tiers": [{
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 1000,
|
||||
"unit_amount_decimal": "1000",
|
||||
"up_to": 99
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 900,
|
||||
"unit_amount_decimal": "900",
|
||||
"up_to": 250
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 800,
|
||||
"unit_amount_decimal": "800",
|
||||
"up_to": 499
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 700,
|
||||
"unit_amount_decimal": "700",
|
||||
"up_to": 999
|
||||
}, {
|
||||
"flat_amount": null,
|
||||
"flat_amount_decimal": null,
|
||||
"unit_amount": 500,
|
||||
"unit_amount_decimal": "500",
|
||||
"up_to": null
|
||||
}],
|
||||
"tiers_mode": "volume",
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRinAxTxXxh2fmnZmorCm2",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143177,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4au1au9F2Ysa0",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": "volume",
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 100,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"subscription_item": "si_J5d3M9tUQgrnPa",
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "subscription"
|
||||
}, {
|
||||
"id": "il_1ITRnUAxTxXxh2fmGn4D2s9W",
|
||||
"object": "line_item",
|
||||
"amount": 5000,
|
||||
"currency": "usd",
|
||||
"description": "50 sip device × registered device (at $1.00 / month)",
|
||||
"discount_amounts": [],
|
||||
"discountable": true,
|
||||
"discounts": [],
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"period": {
|
||||
"end": 1618060195,
|
||||
"start": 1615381795
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRhKAxTxXxh2fmhsYLgyLM",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 100,
|
||||
"amount_decimal": "100",
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1615143086,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4at3poNkI5OSA",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRhKAxTxXxh2fmhsYLgyLM",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1615143086,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4at3poNkI5OSA",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": 100,
|
||||
"unit_amount_decimal": "100"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 50,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"subscription_item": "si_J5d3F3SkebrH4S",
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "subscription"
|
||||
}, {
|
||||
"id": "il_1ITRnUAxTxXxh2fmmUTvs8b0",
|
||||
"object": "line_item",
|
||||
"amount": 0,
|
||||
"currency": "usd",
|
||||
"description": "60 per min × api rate limit (Tier 3 at $0.00 / month)",
|
||||
"discount_amounts": [],
|
||||
"discountable": true,
|
||||
"discounts": [],
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"period": {
|
||||
"end": 1618060195,
|
||||
"start": 1615381795
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": null,
|
||||
"amount_decimal": null,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"tiers": [{
|
||||
"flat_amount": 0,
|
||||
"flat_amount_decimal": "0",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 10
|
||||
}, {
|
||||
"flat_amount": 500,
|
||||
"flat_amount_decimal": "500",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 30
|
||||
}, {
|
||||
"flat_amount": 1000,
|
||||
"flat_amount_decimal": "1000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 60
|
||||
}, {
|
||||
"flat_amount": 2000,
|
||||
"flat_amount_decimal": "2000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 120
|
||||
}, {
|
||||
"flat_amount": 4000,
|
||||
"flat_amount_decimal": "4000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 300
|
||||
}, {
|
||||
"flat_amount": 5000,
|
||||
"flat_amount_decimal": "5000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": null
|
||||
}],
|
||||
"tiers_mode": "volume",
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": "volume",
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 60,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"subscription_item": "si_J5d3bcURInlK5Y",
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "subscription"
|
||||
}, {
|
||||
"id": "il_1ITRnVAxTxXxh2fm6VDudRAm",
|
||||
"object": "line_item",
|
||||
"amount": 1000,
|
||||
"currency": "usd",
|
||||
"description": "api rate limit (Tier 3 at $10.00 / month)",
|
||||
"discount_amounts": [],
|
||||
"discountable": true,
|
||||
"discounts": [],
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"period": {
|
||||
"end": 1618060195,
|
||||
"start": 1615381795
|
||||
},
|
||||
"plan": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": null,
|
||||
"amount_decimal": null,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"tiers": [{
|
||||
"flat_amount": 0,
|
||||
"flat_amount_decimal": "0",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 10
|
||||
}, {
|
||||
"flat_amount": 500,
|
||||
"flat_amount_decimal": "500",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 30
|
||||
}, {
|
||||
"flat_amount": 1000,
|
||||
"flat_amount_decimal": "1000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 60
|
||||
}, {
|
||||
"flat_amount": 2000,
|
||||
"flat_amount_decimal": "2000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 120
|
||||
}, {
|
||||
"flat_amount": 4000,
|
||||
"flat_amount_decimal": "4000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": 300
|
||||
}, {
|
||||
"flat_amount": 5000,
|
||||
"flat_amount_decimal": "5000",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null,
|
||||
"up_to": null
|
||||
}],
|
||||
"tiers_mode": "volume",
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"price": {
|
||||
"id": "price_1ISRgHAxTxXxh2fmUg80e3mw",
|
||||
"object": "price",
|
||||
"active": true,
|
||||
"billing_scheme": "tiered",
|
||||
"created": 1615143021,
|
||||
"currency": "usd",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"product": "prod_J4asDaODFXDjui",
|
||||
"recurring": {
|
||||
"aggregate_usage": null,
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"tiers_mode": "volume",
|
||||
"transform_quantity": null,
|
||||
"type": "recurring",
|
||||
"unit_amount": null,
|
||||
"unit_amount_decimal": null
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 0,
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"subscription_item": "si_J5d3bcURInlK5Y",
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "subscription"
|
||||
}],
|
||||
"has_more": false,
|
||||
"total_count": 4,
|
||||
"url": "/v1/invoices/in_1ITRnUAxTxXxh2fmmch7bUlr/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "0A22219A-0001",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"payment_intent": {
|
||||
"id": "pi_1ITRnUAxTxXxh2fmmyjspFCb",
|
||||
"object": "payment_intent",
|
||||
"amount": 96000,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 96000,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"id": "ch_1ITRnUAxTxXxh2fmvtLpMbNr",
|
||||
"object": "charge",
|
||||
"amount": 96000,
|
||||
"amount_captured": 96000,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_1ITRnVAxTxXxh2fmTGIuTAaB",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "DRACHTIO COMM SVCS LLC",
|
||||
"captured": true,
|
||||
"created": 1615381796,
|
||||
"currency": "usd",
|
||||
"customer": "cus_J5coVe5AQ5UR6h",
|
||||
"description": "Subscription creation",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"invoice": "in_1ITRnUAxTxXxh2fmmch7bUlr",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 16,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_1ITRnUAxTxXxh2fmmyjspFCb",
|
||||
"payment_method": "card_1ITRZIAxTxXxh2fmgZWoCsPx",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": null
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 9,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "KDQxr00TBv7zH8Sg",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"receipt_email": null,
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_1GjPVFAxTxXxh2fm/ch_1ITRnUAxTxXxh2fmvtLpMbNr/rcpt_J5d3fiWQsaYt9Pmkw0SEV34LbxYJyGl",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"object": "list",
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_1ITRnUAxTxXxh2fmvtLpMbNr/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": {
|
||||
"id": "card_1ITRZIAxTxXxh2fmgZWoCsPx",
|
||||
"object": "card",
|
||||
"address_city": null,
|
||||
"address_country": null,
|
||||
"address_line1": null,
|
||||
"address_line1_check": null,
|
||||
"address_line2": null,
|
||||
"address_state": null,
|
||||
"address_zip": null,
|
||||
"address_zip_check": null,
|
||||
"brand": "Visa",
|
||||
"country": "US",
|
||||
"customer": "cus_J5coVe5AQ5UR6h",
|
||||
"cvc_check": null,
|
||||
"dynamic_last4": null,
|
||||
"exp_month": 9,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "KDQxr00TBv7zH8Sg",
|
||||
"funding": "credit",
|
||||
"last4": "4242",
|
||||
"metadata": {},
|
||||
"name": null,
|
||||
"tokenization_method": null
|
||||
},
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Drachtio Comm Svcs LLC",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_1ITRnUAxTxXxh2fmmyjspFCb"
|
||||
},
|
||||
"client_secret": "pi_1ITRnUAxTxXxh2fmmyjspFCb_secret_6FB5iXrTQ3w1f7ckUWeycmYoc",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1615381796,
|
||||
"currency": "usd",
|
||||
"customer": "cus_J5coVe5AQ5UR6h",
|
||||
"description": "Subscription creation",
|
||||
"invoice": "in_1ITRnUAxTxXxh2fmmch7bUlr",
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_action": null,
|
||||
"on_behalf_of": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": ["card"],
|
||||
"receipt_email": null,
|
||||
"review": null,
|
||||
"setup_future_usage": "off_session",
|
||||
"shipping": null,
|
||||
"source": "card_1ITRZIAxTxXxh2fmgZWoCsPx",
|
||||
"statement_descriptor": "Drachtio Comm Svcs LLC",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
},
|
||||
"payment_settings": {
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"period_end": 1615381795,
|
||||
"period_start": 1615381795,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"receipt_number": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": null,
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1615381795,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1615381795,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": "sub_J5d3C56ZGMDZFA",
|
||||
"subtotal": 96000,
|
||||
"tax": null,
|
||||
"tax_percent": null,
|
||||
"total": 96000,
|
||||
"total_discount_amounts": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_pending_invoice_item_invoice": null,
|
||||
"pause_collection": null,
|
||||
"pending_invoice_item_interval": null,
|
||||
"pending_setup_intent": null,
|
||||
"pending_update": null,
|
||||
"plan": null,
|
||||
"quantity": null,
|
||||
"schedule": null,
|
||||
"start_date": 1615381795,
|
||||
"status": "active",
|
||||
"tax_percent": null,
|
||||
"transfer_data": null,
|
||||
"trial_end": null,
|
||||
"trial_start": null
|
||||
}
|
||||
12
test/data/test.json
Normal file
12
test/data/test.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "foobar",
|
||||
"private_key_id": "17a374747574b98284367f1fd2197401c8",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nkdkdkdkdkdkdkdkdkdkdkdkdkddjdjdjdjjEVtS\n8cZeK4w128sbogusVmVARsj5/\nw6Ou08xBX+e1rsKkiaGnzuKhuZczwWMuAwfnBtNQhw2ZUJCDkQfk7tT1100CAi4h\nLAWX/sufHNi9h2z7yCAeRDGqmIM89lRguycKexy8MM4NZRxAIrLs7LIGhTczMFUC\n6TxaFWlXAgMBAAECggEAMHFgrcUn0ATiQcxkeplWom9Ki7gTuhO1wQeOqRtTIZ+d\nCQOigARklx0YTQlsqoG/acLSfeAcfyLWEklULaqJbeghl9KPE4FwPX13VPlZHCMj\nyXUycrjlxw4tHpcy9egDjj5c4XSiUQ/3nNrn0q0EMxFleM3Mhhj1408HifBLH3KY\nKCA+fq+tqmS/daCUgs7jEZCo4z8qFmz0npHUcQ5P7SrsYf00Di9RH8m1Tc9aXU6H\nkMSCfM8/UpPvt+lOQktWjDKP2APowuPyfl4RgX+9jNUm8h2y9jYKeyTelQC0CAUM\nW4ZB6tiDbXOVeP9miXgVwv3WUh4Mrwufar5e0DeqvQKBgQDKZ6Vk7CEVfGQgBoUU\neWo203Tmc2W5qnXRh0pa1VwUtgte+D7l8Lwfc0gCfjbkk7EXKQrQ19jtTwZAzvHz\nsgziQ8H1IDLQBBtknWxuOJbNuronZE6NMWNp+ULu2Y4XN6MnD75sPgtxFMPUUuWI\n5CdXPKweyC+BKC99ukIposGWcwKBgQDGVCYik8nh3fX+BkC1xrFGZ4z3jIqZs8bk\nLFDCiDGwAJTXee2/L5wUJIv4UGoO822OOg+8ftMCtDmdrIkOjsXUPrhbnrGyqZgM\nEC4T3CNDiQXHqxw4tjg4eM5Vcwi6KkuhSAcClz/aaZ1T5jk0QGdF7UpqKcbsURx8\n5hGcscVEjQKBgBXPcV0crLv5+XgR+8knBDEAPDqQ+Mc2/Rck8vgywYdhznvfWDfC\n5yKkc4ABRbz/xTdvrsCuYavAtjXJlvzhlM3U61OUsqUDrEf9Rq/h3S4yDtkrz+Mb\nDVFgELxYKR2LW0NcSPK1BNqcmDWK8Tz9CNg3q3xtqeDLCcMMjRCbfyzNAoGAWlLC\nl2bFP6eNu6XvXJnj7JOGYMtR6BQ3FX2VPjM2pdht8QBnpXWyWH4YfPtqgeqdT3Pj\n7M25nfakcsm8FbQyJqp13cwVU6/nPj80LPlJ2h0SU8/6510dl6J1Hfdo1xgiH46l\nGqn1e6wz6ZzlGoXmQrOB+32RSdja54sEJF/V3pUCgYAoGFZAGVBuWqipbBLdQqNl\n1ppdEliEBhDPq4cZAnNx1lvnmFn8D5bqi+rB8bkqvGcR921AMLDadasHX4BJAJw+\nQoSx1wqy9Zsiaz9EzWUxHtnKFOzMVeVz/RJqH8hNu4xb6Lv50BgTztoO+bGIOAJ/\nVaY6N4gOkiAihhQzsSnLvQ==\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "cloud-speech-testing@foobar.iam.gserviceaccount.com",
|
||||
"client_id": "109945388890626427918",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cloud-speech-testing%40foobarg.iam.gserviceaccount.com"
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
const test = require('blue-tape');
|
||||
//const test = require('tape').test ;
|
||||
const test = require('tape');
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('starting docker network..', (t) => {
|
||||
t.plan(1);
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
|
||||
setTimeout(() => {
|
||||
t.pass('docker started');
|
||||
t.end(err);
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape');
|
||||
const test = require('tape');
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('stopping docker network..', (t) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('./docker_start');
|
||||
require('./create-test-db');
|
||||
require('./sip-gateways');
|
||||
require('./smpp-gateways');
|
||||
require('./service-providers');
|
||||
require('./voip-carriers');
|
||||
require('./accounts');
|
||||
@@ -9,4 +10,7 @@ require('./applications');
|
||||
require('./auth');
|
||||
require('./sbcs');
|
||||
require('./ms-teams');
|
||||
require('./speech-credentials');
|
||||
require('./recent-calls');
|
||||
require('./webapp_tests');
|
||||
require('./docker_stop');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
|
||||
19
test/oauth/gh-get-user.js
Normal file
19
test/oauth/gh-get-user.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('GET', 200);
|
||||
const request = require('request');
|
||||
require('request-debug')(request);
|
||||
|
||||
const test = async() => {
|
||||
request.get('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GH_CODE}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz.us'
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (err) console.log(error);
|
||||
else console.log(body);
|
||||
})
|
||||
};
|
||||
|
||||
test();
|
||||
14
test/oauth/simple_test/index.html
Normal file
14
test/oauth/simple_test/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>Test oauth signup</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a href="https://github.com/login/oauth/authorize?client_id=a075a5889264b8fbc831&state=foobar&allow_signup=false">Github</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -19,19 +19,6 @@ test('phone number tests', async(t) => {
|
||||
/* add service provider, phone number, and voip carrier */
|
||||
const voip_carrier_sid = await createVoipCarrier(request);
|
||||
|
||||
/* provision phone number - failure case: voip_carrier_sid is required */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
number: '15083084809'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 400 && result.body.msg === 'voip_carrier_sid is required',
|
||||
'voip_carrier_sid is required when provisioning a phone number');
|
||||
|
||||
/* provision phone number - failure case: digits only */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -43,37 +30,9 @@ test('phone number tests', async(t) => {
|
||||
voip_carrier_sid
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 400 && result.body.msg === 'phone number must only include digits',
|
||||
'service_provider_sid is required when provisioning a phone number');
|
||||
|
||||
/* provision phone number - failure case: insufficient digits */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
number: '1508308',
|
||||
voip_carrier_sid
|
||||
}
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.statusCode === 400 && result.body.msg === 'invalid phone number: insufficient digits',
|
||||
'invalid phone number: insufficient digits');
|
||||
|
||||
/* provision phone number - failure case: invalid US number */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
number: '150830848091',
|
||||
voip_carrier_sid
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 400 && result.body.msg === 'invalid US phone number',
|
||||
'invalid US phone number');
|
||||
t.ok(result.statusCode === 201,
|
||||
'accepts E.164 format');
|
||||
const sid = result.body.sid;
|
||||
|
||||
/* add a phone number */
|
||||
result = await request.post('/PhoneNumbers', {
|
||||
@@ -86,17 +45,17 @@ test('phone number tests', async(t) => {
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created phone number');
|
||||
const sid = result.body.sid;
|
||||
const sid2 = result.body.sid;
|
||||
|
||||
/* query all phone numbers */
|
||||
result = await request.get('/PhoneNumbers', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1 , 'successfully queried all phone numbers');
|
||||
t.ok(result.length === 2, 'successfully queried all phone numbers');
|
||||
|
||||
/* query one phone numbers */
|
||||
result = await request.get(`/PhoneNumbers/${sid}`, {
|
||||
result = await request.get(`/PhoneNumbers/${sid2}`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
@@ -108,6 +67,10 @@ test('phone number tests', async(t) => {
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted phone number');
|
||||
result = await request.delete(`/PhoneNumbers/${sid2}`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
|
||||
|
||||
79
test/recent-calls.js
Normal file
79
test/recent-calls.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const test = require('tape') ;
|
||||
const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const consoleLogger = {debug: console.log, info: console.log, error: console.error}
|
||||
const {writeCdrs} = require('@jambonz/time-series')(consoleLogger, '127.0.0.1');
|
||||
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
test('recent calls tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const jsonKey = fs.readFileSync(`${__dirname}/data/test.json`, {encoding: 'utf8'});
|
||||
let sid;
|
||||
try {
|
||||
let result;
|
||||
const service_provider_sid = await createServiceProvider(request);
|
||||
const account_sid = await createAccount(request, service_provider_sid);
|
||||
|
||||
const token = jwt.sign({
|
||||
account_sid
|
||||
}, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
const authUser = {bearer: token};
|
||||
|
||||
/* write sample cdr data */
|
||||
const points = 500;
|
||||
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 < 500; 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: account_sid,
|
||||
direction: 0 === i % 2 ? 'inbound' : 'outbound',
|
||||
trunk: 0 === i % 2 ? 'twilio' : 'user'
|
||||
});
|
||||
}
|
||||
|
||||
await writeCdrs(data);
|
||||
t.pass('seeded cdr data');
|
||||
|
||||
/* query last 7 days */
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25`, {
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
|
||||
27
test/sbcs.js
27
test/sbcs.js
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -17,18 +17,6 @@ test('sbc_addresses tests', async(t) => {
|
||||
let result;
|
||||
const service_provider_sid = await createServiceProvider(request);
|
||||
|
||||
/* add a community sbc */
|
||||
result = await request.post('/Sbcs', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
ipv4: '192.168.1.1'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created community sbc ');
|
||||
const sid1 = result.body.sid;
|
||||
|
||||
/* add a service provider sbc */
|
||||
result = await request.post('/Sbcs', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -40,24 +28,17 @@ test('sbc_addresses tests', async(t) => {
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created service provider sbc ');
|
||||
const sid2 = result.body.sid;
|
||||
|
||||
result = await request.get('/Sbcs', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true
|
||||
});
|
||||
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.1', 'successfully retrieved community sbc');
|
||||
const sid = result.body.sid;
|
||||
|
||||
result = await request.get(`/Sbcs?service_provider_sid=${service_provider_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true
|
||||
});
|
||||
//console.log(result.body)
|
||||
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc');
|
||||
|
||||
await deleteObjectBySid(request, '/Sbcs', sid1);
|
||||
await deleteObjectBySid(request, '/Sbcs', sid2);
|
||||
await deleteObjectBySid(request, '/Sbcs', sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
//t.end();
|
||||
|
||||
@@ -45,15 +45,23 @@ const createSchema = () => {
|
||||
const seedDb = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('seeding database..')
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-default-service-provider-and-account.sql`, (err) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/seed-integration-test.sql`, (err) => {
|
||||
if (err) return reject(err);
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err) => {
|
||||
if (err) return reject(err);
|
||||
exec(`node ${__dirname}/../db/reset_admin_password.js`, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resetAdminPassword = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
/* not needed when running jambonz hosting mode */
|
||||
if (process.env.STRIPE_API_KEY) return resolve();
|
||||
console.log('creating admin user..')
|
||||
exec(`node ${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -72,6 +80,7 @@ startDocker()
|
||||
.then(createDb)
|
||||
.then(createSchema)
|
||||
.then(seedDb)
|
||||
.then(resetAdminPassword)
|
||||
.then(() => {
|
||||
console.log('ready for testing!');
|
||||
require('..');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {deleteObjectBySid} = require('./utils');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@@ -94,7 +95,6 @@ test('service provider tests', async(t) => {
|
||||
});
|
||||
t.ok(result.name === 'johndoe' && result.root_domain === 'example.com', 'successfully retrieved service provider by sid');
|
||||
|
||||
|
||||
/* update service providers */
|
||||
result = await request.put(`/ServiceProviders/${sid}`, {
|
||||
auth: authAdmin,
|
||||
@@ -106,6 +106,16 @@ test('service provider tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated service provider');
|
||||
|
||||
/* add a predefined carrier for a service provider */
|
||||
result = await request.post(`/ServiceProviders/${sid}/PredefinedCarriers/7d509a18-bbff-4c5d-b21e-b99bf8f8c49a`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added predefined carrier to service provider');
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', result.body.sid);
|
||||
|
||||
/* delete service providers */
|
||||
result = await request.delete(`/ServiceProviders/${sid}`, {
|
||||
auth: authAdmin,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('blue-tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -25,6 +25,7 @@ test('sip gateway tests', async(t) => {
|
||||
body: {
|
||||
voip_carrier_sid,
|
||||
ipv4: '192.168.1.1',
|
||||
netmask: 32,
|
||||
inbound: true,
|
||||
outbound: true
|
||||
}
|
||||
@@ -34,6 +35,7 @@ test('sip gateway tests', async(t) => {
|
||||
|
||||
/* query all sip gateways */
|
||||
result = await request.get('/SipGateways', {
|
||||
qs: {voip_carrier_sid},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
@@ -55,12 +57,13 @@ test('sip gateway tests', async(t) => {
|
||||
resolveWithFullResponse: true,
|
||||
body: {
|
||||
port: 5061,
|
||||
netmask:24,
|
||||
outbound: false
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated voip carrier');
|
||||
|
||||
/* delete sip gatewas */
|
||||
/* delete sip gateways */
|
||||
result = await request.delete(`/SipGateways/${sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user