Compare commits

..

18 Commits

Author SHA1 Message Date
Dave Horton
6d3fa96e04 add inbound and outbound messaging support 2020-10-09 08:02:33 -04:00
Dave Horton
6efbc49d4f sms working for 3 providers 2020-10-07 14:40:56 -04:00
Dave Horton
cff276402f update deps to clear warnings 2020-10-02 14:01:34 -04:00
Dave Horton
d9f372000e outbound SMS now working 2020-10-02 13:49:46 -04:00
Dave Horton
4da1f4f89d bugfix: createCall REST API to Teams endpoint was being blocked 2020-09-30 15:38:19 -04:00
Dave Horton
8a91333725 inbound SMS 2020-09-30 15:28:12 -04:00
Dave Horton
ee27cf41cb initial changes to support messaging 2020-09-29 15:45:08 -04:00
Dave Horton
491b44709c swagger fixes 2020-09-20 15:49:57 -04:00
Dave Horton
c89ee55389 bugfix #2: pass speech synth language 2020-07-24 15:33:43 -04:00
Dave Horton
f52cf88423 issue with multiple timestamps 2020-07-22 11:57:32 -04:00
Dave Horton
d3c347ac3f schema syntax change 2020-07-22 11:51:19 -04:00
Dave Horton
9df6925b47 travis changes 2020-07-22 11:45:04 -04:00
Dave Horton
5ae6cda12a fix schema problem 2020-07-22 11:35:56 -04:00
Dave Horton
3be0412de1 add list api keys for account, track last_used for api_keys 2020-07-22 11:31:05 -04:00
Dave Horton
4efee5a8b8 add voip_carriers.e164_leading_plus for carrier-level configuration of E.164 dial string 2020-07-16 09:45:59 -04:00
Dave Horton
326b1b673e generate a new admin token as part of reset_admin_password.js 2020-05-31 16:35:02 -04:00
Dave Horton
a746bbc4c9 fix for service provider api 2020-05-29 09:54:26 -04:00
Dave Horton
0e248cb393 add support for ms teams 2020-05-26 08:57:19 -04:00
26 changed files with 930 additions and 146 deletions

View File

@@ -5,4 +5,4 @@ node_js:
services:
- mysql
script:
- npm test
- npm test

32
app.js
View File

@@ -1,7 +1,11 @@
const assert = require('assert');
const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
const express = require('express');
const app = express();
@@ -22,14 +26,16 @@ const {
listCalls,
purgeCalls,
retrieveSet
} = require('jambonz-realtimedb-helpers')({
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
lookupAppBySid,
lookupAccountBySid
} = require('jambonz-db-helpers')({
lookupAccountBySid,
lookupAccountByPhoneNumber,
lookupAppByPhoneNumber
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD,
@@ -49,7 +55,9 @@ Object.assign(app.locals, {
purgeCalls,
retrieveSet,
lookupAppBySid,
lookupAccountBySid
lookupAccountBySid,
lookupAccountByPhoneNumber,
lookupAppByPhoneNumber
});
const unless = (paths, middleware) => {
@@ -60,13 +68,19 @@ const unless = (paths, middleware) => {
};
app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.urlencoded({
extended: true
}));
app.use(express.json());
app.use('/v1', unless(['/login', '/Users'], passport.authenticate('bearer', { session: false })));
app.use('/v1', unless(['/login', '/Users', '/messaging', '/outboundSMS'], passport.authenticate('bearer', {
session: false
})));
app.use('/', routes);
app.use((err, req, res, next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
res.status(err.status || 500).json({
msg: err.message
});
});
logger.info(`listening for HTTP traffic on port ${PORT}`);
app.listen(PORT);

View File

@@ -1,5 +1,6 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS call_routes;
@@ -7,10 +8,10 @@ DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS users;
@@ -48,26 +49,28 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
CREATE TABLE ms_teams_tenants
(
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
account_sid CHAR(36),
application_sid CHAR(36),
tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE api_keys
(
api_key_sid CHAR(36) NOT NULL UNIQUE ,
token CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36),
service_provider_sid CHAR(36),
expires_at TIMESTAMP,
expires_at TIMESTAMP NULL,
last_used TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (api_key_sid)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
CREATE TABLE ms_teams_tenants
(
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
account_sid CHAR(36) NOT NULL,
application_sid CHAR(36),
tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE sbc_addresses
(
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
@@ -94,6 +97,7 @@ name VARCHAR(64) NOT NULL UNIQUE ,
description VARCHAR(255),
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
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,
PRIMARY KEY (voip_carrier_sid)
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -144,8 +148,9 @@ 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',
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account',
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 ',
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
@@ -182,21 +187,21 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENC
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_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 (service_provider_sid) REFERENCES service_providers (service_provider_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 ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_1 (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 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_2 (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_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid);
@@ -238,6 +243,8 @@ ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REF
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
CREATE INDEX name_idx ON service_providers (name);
CREATE INDEX root_domain_idx ON service_providers (root_domain);
@@ -251,3 +258,5 @@ ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provi
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_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;

View File

@@ -48,7 +48,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[57655C0C-9427-4CC7-9502-24ACF56AAECF]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[2A735FAB-592C-42E5-9C8B-06B109314799]]></uid>
</SQLTable>
@@ -63,7 +63,7 @@
</location>
<size>
<width>266.00</width>
<height>120.00</height>
<height>140.00</height>
</size>
<zorder>6</zorder>
<SQLField>
@@ -119,7 +119,14 @@
<objectComment><![CDATA[If provided, all incoming calls from this source will be routed to the associated application]]></objectComment>
<uid><![CDATA[B6545E2E-7F55-4082-AEFA-29F50C137D64]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<SQLField>
<name><![CDATA[e164_leading_plus]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[false]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[123EA4AC-627B-42A1-8779-D72494E8D47F]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[8]]></labelWindowIndex>
<objectComment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[3D3136A7-AFC0-4A70-AEC3-68577955CA2E]]></uid>
@@ -130,12 +137,12 @@
<comment><![CDATA[An authorization token that is used to access the REST api]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>1302.00</x>
<y>61.00</y>
<x>1319.00</x>
<y>38.00</y>
</location>
<size>
<width>252.00</width>
<height>120.00</height>
<width>245.00</width>
<height>160.00</height>
</size>
<zorder>1</zorder>
<SQLField>
@@ -187,9 +194,22 @@
<SQLField>
<name><![CDATA[expires_at]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NULL]]></defaultValue>
<uid><![CDATA[DE86BC18-858E-4D7E-9B83-891DB2861434]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
<SQLField>
<name><![CDATA[last_used]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NULL]]></defaultValue>
<uid><![CDATA[11A93288-B892-436B-9BB4-D5C3B70DB061]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[created_at]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[CURRENT_TIMESTAMP]]></defaultValue>
<uid><![CDATA[C84C9B6A-80B5-4B0B-8C14-EB02F7421BBE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[13]]></labelWindowIndex>
<objectComment><![CDATA[An authorization token that is used to access the REST api]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[3EDF89A0-FD38-4DF9-BB65-E0FCD0A678BE]]></uid>
@@ -240,7 +260,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[04BB457A-D532-4780-8A58-5900094171EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[2]]></labelWindowIndex>
<labelWindowIndex><![CDATA[4]]></labelWindowIndex>
<objectComment><![CDATA[An HTTP callback]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></uid>
@@ -251,8 +271,8 @@
<comment><![CDATA[a regex-based pattern match for call routing]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>406.00</x>
<y>425.00</y>
<x>424.00</x>
<y>461.00</y>
</location>
<size>
<width>254.00</width>
@@ -311,7 +331,7 @@
<uid><![CDATA[9B4208B5-9E3B-4B76-B7F7-4E5D36B99BF2]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<labelWindowIndex><![CDATA[12]]></labelWindowIndex>
<objectComment><![CDATA[a regex-based pattern match for call routing]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[78584D93-2CD7-4495-9C5E-893C7B869133]]></uid>
@@ -364,6 +384,7 @@
<destinationCardinality>2</destinationCardinality>
<referencesFieldUID><![CDATA[1342FAFA-C15C-429B-809B-C6C55F9FA5B6]]></referencesFieldUID>
<referencesTableUID><![CDATA[985D6997-B1A7-4AB3-80F4-4D59B45480C8]]></referencesTableUID>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[EB48F39F-9D5F-43E0-BE8A-34A5C1304A76]]></uid>
</SQLField>
<SQLField>
@@ -388,7 +409,9 @@
<uid><![CDATA[1DDAD1A1-942D-4487-89C8-D496B7F82274]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[2]]></labelWindowIndex>
<objectComment><![CDATA[A Microsoft Teams customer tenant]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[92FD042A-5AEC-4D8F-AB94-C73C0F566F75]]></uid>
</SQLTable>
<SQLTable>
@@ -455,7 +478,7 @@
<objectComment><![CDATA[lower priority carriers are attempted first]]></objectComment>
<uid><![CDATA[01F61C68-799B-49B0-9E6A-0E2162EE5A54]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<labelWindowIndex><![CDATA[7]]></labelWindowIndex>
<objectComment><![CDATA[An entry in the LCR routing list]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[956025F5-0798-47F7-B76C-457814C7B52E]]></uid>
@@ -553,7 +576,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[C7130A90-DBB4-424D-A9A9-CB203C32350C]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
<objectComment><![CDATA[An enterprise that uses the platform for comm services]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[985D6997-B1A7-4AB3-80F4-4D59B45480C8]]></uid>
@@ -635,7 +658,7 @@
<uid><![CDATA[962CB80A-54CB-4C6A-9591-9BFC644CF80F]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[8]]></labelWindowIndex>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<objectComment><![CDATA[A phone number that has been assigned to an account]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[BA650DDC-AC7B-4DFE-A5E5-828C75607807]]></uid>
@@ -727,7 +750,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[1C744DE3-39BD-4EC6-B427-7EB2DD258771]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[4]]></labelWindowIndex>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<objectComment><![CDATA[A whitelisted sip gateway used for origination/termination]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[D8A564E2-DA41-4217-8ACE-06CF77E9BEC1]]></uid>
@@ -743,7 +766,7 @@
</location>
<size>
<width>345.00</width>
<height>240.00</height>
<height>260.00</height>
</size>
<zorder>0</zorder>
<SQLField>
@@ -791,7 +814,7 @@
<destinationCardinality>2</destinationCardinality>
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
<objectComment><![CDATA[webhook to call for inbound calls to phone numbers owned by this account]]></objectComment>
<objectComment><![CDATA[webhook to call for inbound calls ]]></objectComment>
<uid><![CDATA[55AE0F31-209A-49F9-A6CB-1BB31BE4178A]]></uid>
</SQLField>
<SQLField>
@@ -808,6 +831,20 @@
<objectComment><![CDATA[webhook to call for call status events]]></objectComment>
<uid><![CDATA[A3B4621B-DF13-4920-A718-068B20CB4E8A]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[messaging_hook_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>webhook_sid</referencesField>
<referencesTable>webhooks</referencesTable>
<referencesField><![CDATA[webhook_sid]]></referencesField>
<referencesTable><![CDATA[webhooks]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
<objectComment><![CDATA[webhook to call for inbound SMS/MMS ]]></objectComment>
<uid><![CDATA[4690A98A-2F67-4205-A5C1-D9523F6B3282]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_synthesis_vendor]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
@@ -860,7 +897,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[3FDDDF3B-375D-4DE4-B759-514438845F7D]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[7]]></labelWindowIndex>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<objectComment><![CDATA[A defined set of behaviors to be applied to phone calls ]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[E97EE4F0-7ED7-4E8C-862E-D98192D6EAE0]]></uid>
@@ -930,6 +967,8 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[6F249D1F-111F-45B4-B76C-8B5E6B9CB43F]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0EE651E-DBF6-4CAC-A517-AC85BCC2D3AF]]></uid>
</SQLTable>
<SQLTable>
@@ -973,7 +1012,7 @@
<uid><![CDATA[B73773BA-AB1B-47AA-B995-2D2FE006198F]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<objectComment><![CDATA[Least cost routing table]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F283D572-F670-4571-91FD-A665A9D3E15D]]></uid>
@@ -1060,17 +1099,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1753.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1924.000000]]></RightSidebarWidth>
<sidebarIndex><![CDATA[2]]></sidebarIndex>
<snapToGrid><![CDATA[0]]></snapToGrid>
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[1298.000000]]></windowHeight>
<windowLocationX><![CDATA[192.000000]]></windowLocationX>
<windowLocationY><![CDATA[74.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{298.5, 2}]]></windowScrollOrigin>
<windowWidth><![CDATA[2153.000000]]></windowWidth>
<windowHeight><![CDATA[1013.000000]]></windowHeight>
<windowLocationX><![CDATA[2716.000000]]></windowLocationX>
<windowLocationY><![CDATA[1913.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 5}]]></windowScrollOrigin>
<windowWidth><![CDATA[2201.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>

View File

@@ -6,6 +6,10 @@ const sqlInsert = `INSERT into users
(user_sid, name, hashed_password, salt)
values (?, ?, ?, ?)
`;
const sqlChangeAdminToken = `UPDATE api_keys set token = ?
WHERE account_sid IS NULL
AND service_provider_sid IS NULL`;
/**
* generates random string of characters i.e salt
* @function
@@ -55,8 +59,13 @@ getMysqlConnection((err, conn) => {
], (err) => {
if (err) return console.log(err, 'Error inserting admin user');
console.log('successfully reset admin password');
conn.release();
process.exit(0);
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);
});
});
});
});

View File

@@ -10,6 +10,7 @@ function transmogrifyResults(results) {
const obj = row.acc;
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
Object.assign(obj, {registration_hook: row.rh});
delete obj.registration_hook.webhook_sid;
}
else obj.registration_hook = null;
delete obj.registration_hook_sid;

View File

@@ -1,11 +1,53 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
class ApiKey extends Model {
constructor() {
super();
}
/**
* list all api keys for an account
*/
static retrieveAll(account_sid) {
const sql = account_sid ?
'SELECT * from api_keys WHERE account_sid = ?' :
'SELECT * from api_keys WHERE account_sid IS NULL';
const args = account_sid ? [account_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);
});
});
});
}
/**
* update last_used api key for an account
*/
static updateLastUsed(account_sid) {
const sql = 'UPDATE api_keys SET last_used = NOW() WHERE account_sid = ?';
const args = [account_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);
});
});
});
}
}
ApiKey.table = 'api_keys';
ApiKey.fields = [
{
@@ -25,6 +67,18 @@ ApiKey.fields = [
{
name: 'service_provider_sid',
type: 'string'
},
{
name: 'expires_at',
type: 'date'
},
{
name: 'created_at',
type: 'date'
},
{
name: 'last_used',
type: 'date'
}
];

View File

@@ -1,29 +1,13 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const listSqlSp = `
SELECT * from applications
WHERE account_sid in (
SELECT account_sid from accounts
WHERE service_provider_sid = ?
)`;
const listSqlAccount = 'SELECT * from applications WHERE account_sid = ?';
const retrieveSqlSp = `
SELECT * from applications
WHERE account_sid in (
SELECT account_sid from accounts
WHERE service_provider_sid = ?
)
AND application_sid = ?`;
const retrieveSqlAccount = `
SELECT * from applications
WHERE account_sid = ?
AND application_sid = ?`;
const retrieveSql = `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`;
ON app.call_status_hook_sid = sh.webhook_sid
LEFT JOIN webhooks AS mh
ON app.messaging_hook_sid = mh.webhook_sid`;
function transmogrifyResults(results) {
return results.map((row) => {
@@ -36,8 +20,13 @@ function transmogrifyResults(results) {
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;
});
}
@@ -123,12 +112,14 @@ Application.fields = [
{
name: 'call_hook_sid',
type: 'string',
required: true
},
{
name: 'call_status_hook_sid',
type: 'string',
required: true
},
{
name: 'messaging_hook_sid',
type: 'string',
}
];

View File

@@ -1,9 +1,65 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const retrieveSql = `SELECT * from service_providers sp
LEFT JOIN webhooks AS rh
ON sp.registration_hook_sid = rh.webhook_sid`;
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.sp;
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
Object.assign(obj, {registration_hook: row.rh});
delete obj.registration_hook.webhook_sid;
}
else obj.registration_hook = null;
delete obj.registration_hook_sid;
return obj;
});
}
class ServiceProvider extends Model {
constructor() {
super();
}
/**
* list all service providers
*/
static retrieveAll() {
const sql = retrieveSql;
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql, nestTables: true}, [], (err, results, fields) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
}
/**
* retrieve a service provider
*/
static retrieve(sid) {
const args = [sid];
const sql = `${retrieveSql} WHERE sp.service_provider_sid = ?`;
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
}
}
ServiceProvider.table = 'service_providers';
@@ -27,7 +83,11 @@ ServiceProvider.fields = [
type: 'string',
},
{
name: 'registration_hook',
name: 'registration_hook_sid',
type: 'string',
},
{
name: 'ms_teams_fqdn',
type: 'string',
}

View File

@@ -20,7 +20,8 @@ MsTeamsTenant.fields = [
},
{
name: 'account_sid',
type: 'string'
type: 'string',
required: true
},
{
name: 'application_sid',

View File

@@ -21,6 +21,18 @@ VoipCarrier.fields = [
{
name: 'description',
type: 'string'
},
{
name: 'account_sid',
type: 'string',
},
{
name: 'application_sid',
type: 'string'
},
{
name: 'e164_leading_plus',
type: 'number'
}
];

View File

@@ -3,7 +3,9 @@ const request = require('request');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const ApiKey = require('../../models/api-key');
const ServiceProvider = require('../../models/service-provider');
const uuidv4 = require('uuid/v4');
const decorate = require('./decorate');
const snakeCase = require('../../utils/snake-case');
const sysError = require('./error');
@@ -27,6 +29,15 @@ function coerceNumbers(callInfo) {
return callInfo;
}
async function updateLastUsed(logger, sid, req) {
if (req.user.hasAdminAuth || req.user.hasServiceProviderAuth) return;
try {
await ApiKey.updateLastUsed(sid);
} catch (err) {
logger.error({err}, `Error updating last used for accountSid ${sid}`);
}
}
function validateUpdateCall(opts) {
// only one type of update can be supplied per request
const hasWhisper = opts.whisper;
@@ -65,6 +76,7 @@ function validateTo(to) {
if (to && typeof to === 'object') {
switch (to.type) {
case 'phone':
case 'teams':
if (typeof to.number === 'string') return;
break;
case 'user':
@@ -95,6 +107,7 @@ async function validateCreateCall(logger, sid, req) {
call_hook: application.call_hook,
call_status_hook: application.call_status_hook,
speech_synthesis_vendor: application.speech_synthesis_vendor,
speech_synthesis_language: application.speech_synthesis_language,
speech_synthesis_voice: application.speech_synthesis_voice,
speech_recognizer_vendor: application.speech_recognizer_vendor,
speech_recognizer_language: application.speech_recognizer_language
@@ -112,6 +125,7 @@ async function validateCreateCall(logger, sid, req) {
Object.assign(obj, {
speech_synthesis_vendor: 'google',
speech_synthesis_voice: 'en-US-Wavenet-C',
speech_synthesis_language: 'en-US',
speech_recognizer_vendor: 'google',
speech_recognizer_language: 'en-US'
});
@@ -122,6 +136,30 @@ async function validateCreateCall(logger, sid, req) {
}
}
async function validateCreateMessage(logger, sid, req) {
const obj = req.body;
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);
const from = arr ? arr[1] : obj.from;
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) {
throw new DbErrorBadRequest('either text or media required in outbound message');
}
}
async function validateAdd(req) {
/* account-level token can not be used to add accounts */
if (req.user.hasAccountAuth) {
@@ -251,6 +289,19 @@ router.put('/:sid', async(req, res) => {
return res.status(404).end();
}
res.status(204).end();
updateLastUsed(logger, sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
/* retrieve account level api keys */
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ApiKey.retrieveAll(req.params.sid);
res.status(200).json(results);
updateLastUsed(logger, req.params.sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -276,6 +327,7 @@ router.post('/:sid/Calls', async(req, res) => {
await validateCreateCall(logger, sid, req);
logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
@@ -308,6 +360,7 @@ router.get('/:sid/Calls', async(req, res) => {
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -331,6 +384,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -354,6 +408,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -384,10 +439,53 @@ router.post('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`updateCall: call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
/**
* create a new Message
*/
router.post('/:sid/Messages', async(req, res) => {
const sid = req.params.sid;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createMessage API request');
return res.json({msg: 'no available feature servers at this time'}).status(500);
}
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 payload = Object.assign({messageSid: uuidv4()}, req.body);
logger.debug({payload}, `sending createMessage API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: payload
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createMessage POST to ${ip}`);
return res.sendStatus(500);
}
if (response.statusCode !== 200) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -62,7 +62,7 @@ router.post('/', async(req, res) => {
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['call_hook', 'call_status_hook']) {
for (const prop of ['call_hook', 'call_status_hook', 'messaging_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
@@ -113,7 +113,7 @@ router.put('/:sid', async(req, res) => {
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['call_hook', 'call_status_hook']) {
for (const prop of ['call_hook', 'call_status_hook', 'messaging_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];

View File

@@ -20,4 +20,8 @@ api.use('/Sbcs', isAdminScope, require('./sbcs'));
api.use('/Users', require('./users'));
api.use('/login', require('./login'));
// messaging
api.use('/messaging', require('./sms-inbound')); // inbound SMS from carrier
api.use('/outboundSMS', require('./sms-outbound')); // outbound SMS from feature server
module.exports = api;

View File

@@ -31,13 +31,13 @@ async function validateAdd(req) {
async function checkInUse(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (phoneNumber.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
}
}
/* can not change number or voip carrier */
async function validateUpdate(req, sid) {
const result = await PhoneNumber.retrieve(sid);
//const result = await PhoneNumber.retrieve(sid);
if (req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid may not be modified');
if (req.body.number) throw new DbErrorBadRequest('number may not be modified');

View File

@@ -1,6 +1,8 @@
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 decorate = require('./decorate');
const preconditions = {
'delete': noActiveAccounts
@@ -12,6 +14,87 @@ async function noActiveAccounts(req, sid) {
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
}
decorate(router, ServiceProvider, ['*'], preconditions);
decorate(router, ServiceProvider, ['delete'], preconditions);
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
}
}
//logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
const uuid = await ServiceProvider.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
sysError(logger, res, err);
}
});
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieveAll();
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 results = await ServiceProvider.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);
}
});
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];
delete obj[prop]['webhook_sid'];
await Webhook.update(sid, obj[prop]);
}
else {
const sid = await Webhook.make(obj[prop]);
obj[`${prop}_sid`] = sid;
}
}
else {
obj[`${prop}_sid`] = null;
}
delete obj[prop];
}
const rowsAffected = await ServiceProvider.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -0,0 +1,142 @@
const router = require('express').Router();
const request = require('request');
const getProvider = require('../../utils/sms-provider');
const uuidv4 = require('uuid/v4');
const sysError = require('./error');
let idx = 0;
async function doSendResponse(res, respondFn, body) {
if (typeof respondFn === 'number') res.sendStatus(respondFn);
else if (typeof respondFn !== 'function') res.sendStatus(200);
else {
const payload = await respondFn(body);
res.status(200).json(payload);
}
}
router.post('/:provider', async(req, res) => {
const provider = req.params.provider;
const {
retrieveSet,
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for provider module
const arr = getProvider(logger, provider);
if (!arr) {
logger.info({body: req.body, params: req.params},
`rejecting incomingSms request from unknown provider ${provider}`
);
return res.sendStatus(404);
}
const providerData = arr[1];
if (!providerData || !providerData.module) {
logger.info({body: req.body, params: req.params},
`rejecting incomingSms request from badly configured provider ${provider}`
);
return res.sendStatus(404);
}
// load provider module
let filterFn, respondFn;
try {
const {
fromProviderFormat,
formatProviderResponse
} = require(providerData.module);
// must at least provide a filter function
if (!fromProviderFormat) {
logger.info(
`missing fromProviderFormat function in module ${providerData.module} for provider ${provider}`
);
return res.sendStatus(404);
}
filterFn = fromProviderFormat;
respondFn = formatProviderResponse;
} catch (err) {
logger.info(
err,
`failure loading module ${providerData.module} for provider ${provider}`
);
return res.sendStatus(500);
}
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createCall API request');
return res
.json({
msg: 'no available feature servers at this time'
})
.status(480);
}
const ip = fs[idx++ % fs.length];
const serviceUrl = `http://${ip}:3000/v1/messaging/${provider}`;
const messageSid = uuidv4();
const payload = await Promise.resolve(filterFn({messageSid}, req.body));
/**
* lookup the application associated with the number in the To field
* since there could be multiple Tos, we have to search through (and cc also)
*/
let app;
const to = Array.isArray(payload.to) ? payload.to : [payload.to];
const cc = Array.isArray(payload.cc) ? payload.cc : (payload.cc ? [payload.cc] : []);
const dids = to.concat(cc).filter((n) => n.length);
for (let did of dids) {
const regex = /^\+(\d+)$/;
const arr = regex.exec(did);
did = arr ? arr[1] : did;
const obj = await lookupAppByPhoneNumber(did);
logger.info({obj}, `lookup app for phone number ${did}`);
if (obj) {
logger.info({did, obj}, 'Found app for DID');
app = obj;
break;
}
}
if (!app) {
logger.info({payload}, 'No application found for incoming SMS');
return res.sendStatus(404);
}
if (!app.messaging_hook) {
logger.info({payload}, `app "${app.name}" found for incoming SMS does not have an associated messaging hook`);
return res.sendStatus(404);
}
payload.app = app;
logger.debug({body: req.body, payload}, 'filtered incoming SMS');
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${ip}`);
request({
url: serviceUrl,
method: 'POST',
json: true,
body: payload,
},
async(err, response, body) => {
if (err) {
logger.error(err, `Error sending incomingSms POST to ${ip}`);
return res.sendStatus(500);
}
if (200 === response.statusCode) {
// success
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
}
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${ip}`);
return res.sendStatus(500);
});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -0,0 +1,44 @@
const router = require('express').Router();
const getProvider = require('../../utils/sms-provider');
const sysError = require('./error');
router.post('/', async(req, res) => {
const { logger } = req.app.locals;
try {
// if provider specified use it, otherwise use first in list
const arr = getProvider(logger, req.body.provider);
if (!Array.isArray(arr)) {
throw new Error('outboundSMS - unable to locate sms provider to use to send message');
}
const providerData = arr[1];
if (!providerData || !providerData.module) {
throw new Error(`rejecting outgoingSms request for unknown or badly configured provider ${req.body.provider}`);
}
const provider = arr[0];
const opts = providerData.options;
if (!opts || !opts.url) {
throw new Error(`rejecting outgoingSms request -- no HTTP url for ${req.body.provider}`);
}
// load provider module
const { sendSms } = require(providerData.module);
if (!sendSms) {
throw new Error(`missing sendSms function in module ${providerData.module} for provider ${provider}`);
}
// send the SMS
const payload = req.body;
delete payload.provider;
logger.debug({opts, payload}, `outboundSMS - sending to ${opts.url}`);
const response = await sendSms(opts, payload);
logger.info({response, payload: req.body}, `outboundSMS - sent to ${opts.url}`);
res.status(200).json(response);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -37,7 +37,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulApiKeyAdd'
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
@@ -222,7 +222,8 @@ paths:
description: password successfully changed
content:
application/json:
schema: '#/components/schemas/Login'
schema:
$ref: '#/components/schemas/Login'
403:
description: password change failed
content:
@@ -250,6 +251,8 @@ paths:
description: voip carrier name
description:
type: string
e164_leading_plus:
type: boolean
required:
- name
responses:
@@ -687,16 +690,12 @@ paths:
description: root domain for group of accounts that share a registration hook
example: example.com
registration_hook:
type: string
format: url
$ref: '#/components/schemas/Webhook'
description: authentication webhook for registration
example: https://mycompany.com
hook_basic_auth_user:
ms_teams_fqdn:
type: string
description: username to use for http basic auth when calling hook
hook_basic_auth_password:
type: string
description: password to use for http basic auth when calling hook
description: SBC domain name for Microsoft Teams
example: contoso.com
required:
- name
responses:
@@ -1203,6 +1202,34 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/ApiKeys:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
get:
summary: get all api keys for an account
operationId: getAccountApiKeys
responses:
200:
description: list of api keys
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ApiKey'
404:
description: account not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Applications:
post:
@@ -1222,10 +1249,13 @@ paths:
format: uuid
call_hook:
$ref: '#/components/schemas/Webhook'
description: authentication webhook for inbound calls from PSTN
description: application webhook to handle inbound voice calls
call_status_hook:
$ref: '#/components/schemas/Webhook'
description: webhook for call status events
description: webhook to report call status events
messaging_hook:
$ref: '#/components/schemas/Webhook'
description: application webhook to handle inbound SMS/MMS messages
speech_synthesis_vendor:
type: string
speech_synthesis_voice:
@@ -1406,9 +1436,9 @@ paths:
application/json:
schema:
required:
- callSid
- sid
properties:
callSid:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
@@ -1438,8 +1468,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Calls/{CallSid}:
parameters:
- name: AccountSid
@@ -1505,8 +1533,6 @@ paths:
properties:
call_hook:
$ref: '#/components/schemas/Webhook'
call_status_hook:
$ref: '#/components/schemas/Webhook'
call_status:
type: string
enum:
@@ -1517,6 +1543,11 @@ paths:
enum:
- pause
- resume
mute_status:
type: string
enum:
- mute
- unmute
whisper:
$ref: '#/components/schemas/Webhook'
responses:
@@ -1537,7 +1568,39 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Messages:
post:
summary: create an outgoing SMS message
operationId: createMessage
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
responses:
201:
description: call successfully created
content:
application/json:
schema:
required:
- sid
properties:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
providerResponse:
type: string
400:
description: bad request
components:
securitySchemes:
bearerAuth:
@@ -1599,12 +1662,9 @@ components:
type: string
root_domain:
type: string
hook_basic_auth_user:
type: string
format: url
hook_basic_auth_password:
type: string
format: url
registration_hook:
$ref: '#/components/schemas/Webhook'
description: authentication webhook for registration
ms_teams_fqdn:
type: string
required:
@@ -1620,6 +1680,8 @@ components:
type: string
description:
type: string
e164_leading_plus:
type: boolean
required:
- voip_carrier_sid
- name
@@ -1686,10 +1748,13 @@ components:
format: uuid
call_hook:
$ref: '#/components/schemas/Webhook'
description: authentication webhook for registration
description: application webhook for inbound voice calls
call_status_hook:
$ref: '#/components/schemas/Webhook'
description: authentication webhook for registration
description: webhhok for reporting call status events
messaging_hook:
$ref: '#/components/schemas/Webhook'
description: application webhook for inbound SMS/MMS
speech_synthesis_vendor:
type: string
speech_synthesis_voice:
@@ -1702,8 +1767,30 @@ components:
- application_sid
- name
- account_sid
- inbound_hook
- inbound_status_hook
ApiKey:
type: object
properties:
api_key_sid:
type: string
format: uuid
token:
type: string
format: uuid
account_sid:
type: string
format: uuid
service_provider_sid:
type: string
format: uuid
expires_at:
type: dateTime
created_at:
type: dateTime
last_used:
type: dateTime
required:
- api_key_sid
- token
PhoneNumber:
type: object
properties:
@@ -1861,6 +1948,22 @@ components:
required:
- type
example: {"type": "phone", "number": "+16172375080"}
Message:
properties:
provider:
type: string
from:
type: string
to:
type: string
text:
type: string
media:
type: string
required:
- from
- to
example: {"from": "13394445678", "to": "16173333456", "text": "please call when you can"}
security:
- bearerAuth: []

40
lib/utils/sms-provider.js Normal file
View File

@@ -0,0 +1,40 @@
const providers = new Map();
let init = false;
function initProviders(logger) {
if (init) return;
if (process.env.JAMBONES_MESSAGING) {
try {
const obj = JSON.parse(process.env.JAMBONES_MESSAGING);
for (const [key, value] of Object.entries(obj)) {
logger.debug({config: value}, `Adding SMS provider ${key}`);
providers.set(key, value);
}
logger.info(`Configured ${providers.size} SMS providers`);
} catch (err) {
logger.error(err, `expected JSON for JAMBONES_MESSAGING : ${process.env.JAMBONES_MESSAGING}`);
}
}
else {
logger.info('no JAMBONES_MESSAGING env var, messaging is disabled');
}
init = true;
}
function getProvider(logger, partner) {
initProviders(logger);
if (typeof partner === 'string') {
const config = providers.get(partner);
const arr = [partner, config];
logger.debug({arr}, 'getProvider by name');
return arr;
}
else if (providers.size) {
const arr = providers.entries().next().value;
logger.debug({arr}, 'getProvider by first available');
return arr;
}
}
module.exports = getProvider;

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "1.1.5",
"version": "1.2.0",
"description": "",
"main": "app.js",
"scripts": {
@@ -15,27 +15,31 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@jambonz/db-helpers": "^0.5.1",
"@jambonz/messaging-382com": "0.0.2",
"@jambonz/messaging-peerless": "0.0.9",
"@jambonz/messaging-simwood": "0.0.4",
"@jambonz/realtimedb-helpers": "0.2.17",
"cors": "^2.8.5",
"express": "^4.17.1",
"jambonz-db-helpers": "^0.3.4",
"jambonz-realtimedb-helpers": "0.2.4",
"mysql2": "^2.0.2",
"passport": "^0.4.0",
"mysql2": "^2.1.0",
"passport": "^0.4.1",
"passport-http-bearer": "^1.0.1",
"pino": "^5.14.0",
"request": "^2.88.0",
"pino": "^5.17.0",
"request": "^2.88.2",
"request-debug": "^0.2.0",
"swagger-ui-express": "^4.1.2",
"uuid": "^3.3.3",
"swagger-ui-dist": "^3.35.0",
"swagger-ui-express": "^4.1.4",
"uuid": "^3.4.0",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^6.8.0",
"eslint": "^7.10.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^15.0.1",
"request-promise-native": "^1.0.8",
"nyc": "^15.1.0",
"request-promise-native": "^1.0.9",
"tap-dot": "^2.0.0",
"tap-spec": "^5.0.0",
"tape": "^4.13.2"
"tape": "^5.0.1"
}
}

View File

@@ -4,7 +4,11 @@ const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createVoipCarrier, createServiceProvider, createPhoneNumber, deleteObjectBySid} = require('./utils');
const {
createVoipCarrier,
createServiceProvider,
createPhoneNumber,
deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -38,6 +42,26 @@ test('account tests', async(t) => {
t.ok(result.statusCode === 201, 'successfully created account');
const sid = result.body.sid;
/* add an account level api key */
result = await request.post(`/ApiKeys`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
account_sid: sid
}
});
t.ok(result.statusCode === 201 && result.body.token, 'successfully created account level token');
const apiKeySid = result.body.sid;
const accountLevelToken = result.body.token;
/* query all account level api keys */
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
auth: {bearer: accountLevelToken},
json: true,
});
t.ok(Array.isArray(result) && result.length === 1, 'successfully queried account level keys');
/* query all accounts */
result = await request.get('/Accounts', {
auth: authAdmin,
@@ -45,7 +69,7 @@ test('account tests', async(t) => {
});
let regHook = result[0].registration_hook;
t.ok(result.length === 1 &&
Object.keys(regHook).length == 5, 'successfully queried all accounts');
Object.keys(regHook).length == 4, 'successfully queried all accounts');
/* query one accounts */
result = await request.get(`/Accounts/${sid}`, {
@@ -54,9 +78,9 @@ test('account tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved account by sid');
/* update accounts */
/* update account with account level token */
result = await request.put(`/Accounts/${sid}`, {
auth: authAdmin,
auth: {bearer: accountLevelToken},
json: true,
resolveWithFullResponse: true,
body: {
@@ -67,14 +91,21 @@ test('account tests', async(t) => {
}
}
});
t.ok(result.statusCode === 204, 'successfully updated account');
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
/* verify that account level api key last_used was updated*/
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
auth: {bearer: accountLevelToken},
json: true,
});
t.ok(typeof result[0].last_used === 'string', 'api_key last_used timestamp was updated');
result = await request.get(`/Accounts/${sid}`, {
auth: authAdmin,
json: true,
});
//console.log(`retrieved account after update: ${JSON.stringify(result)}`);
t.ok(Object.keys(result.registration_hook).length === 5, 'successfully removed a hook from account');
t.ok(Object.keys(result.registration_hook).length === 4, 'successfully removed a hook from account');
/* assign phone number to account */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {
@@ -97,6 +128,7 @@ test('account tests', async(t) => {
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,
@@ -109,7 +141,7 @@ test('account tests', async(t) => {
t.end();
}
catch (err) {
//console.error(err);
console.error(err);
t.end(err);
}
});

View File

@@ -38,6 +38,9 @@ test('application tests', async(t) => {
call_status_hook: {
url: 'http://example.com/status',
method: 'POST'
},
messaging_hook: {
url: 'http://example.com/sms'
}
}
});
@@ -58,6 +61,7 @@ test('application tests', async(t) => {
json: true,
});
t.ok(result[0].name === 'daveh' , 'successfully retrieved application by sid');
t.ok(result[0].messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
/* update applications */
result = await request.put(`/Applications/${sid}`, {
@@ -67,11 +71,21 @@ test('application tests', async(t) => {
body: {
call_hook: {
url: 'http://example2.com'
},
messaging_hook: {
url: 'http://example2.com/mms'
}
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
/* validate messaging hook was updated */
result = await request.get(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result[0].messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
/* assign phone number to application */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {
auth: authAdmin,

View File

@@ -4,7 +4,7 @@ const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createServiceProvider, deleteObjectBySid} = require('./utils');
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -16,6 +16,8 @@ test('sbc_addresses tests', async(t) => {
try {
let result;
const service_provider_sid = await createServiceProvider(request);
const account_sid = await createAccount(request, service_provider_sid);
const account_sid2 = await createAccount(request, service_provider_sid, 'account2');
/* add a tenant */
result = await request.post('/MicrosoftTeamsTenants', {
@@ -24,6 +26,7 @@ test('sbc_addresses tests', async(t) => {
json: true,
body: {
service_provider_sid,
account_sid,
tenant_fqdn: 'foo.bar.baz'
}
});
@@ -37,6 +40,7 @@ test('sbc_addresses tests', async(t) => {
json: true,
body: {
service_provider_sid,
account_sid: account_sid2,
tenant_fqdn: 'junk.bar.baz'
}
});
@@ -50,8 +54,29 @@ test('sbc_addresses tests', async(t) => {
});
t.ok(result.body.length === 2, 'successfully retrieved tenants');
/* update tenant */
result = await request.put(`/MicrosoftTeamsTenants/${sid1}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
tenant_fqdn: 'foo.bar.bazzle'
}
});
t.ok(result.statusCode === 204, 'successfully updated ms teams tenant');
/* get tenant */
result = await request.get(`/MicrosoftTeamsTenants/${sid1}`, {
auth: authAdmin,
json: true
});
t.ok(result.tenant_fqdn === 'foo.bar.bazzle', 'successfully retrieved ms teams tenant');
await deleteObjectBySid(request, '/MicrosoftTeamsTenants', sid1);
await deleteObjectBySid(request, '/MicrosoftTeamsTenants', sid2);
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/Accounts', account_sid2);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();

View File

@@ -30,7 +30,8 @@ test('service provider tests', async(t) => {
auth: authAdmin,
json: true,
body: {
name: 'daveh'
name: 'daveh',
ms_teams_fqdn: 'contoso.com'
}
});
t.ok(result.statusCode === 201, 'successfully created service provider');
@@ -43,7 +44,10 @@ test('service provider tests', async(t) => {
json: true,
body: {
name: 'johndoe',
root_domain: 'example.com'
root_domain: 'example.com',
registration_hook: {
url: 'http://a.com'
}
}
});
t.ok(result.statusCode === 201, 'successfully created service provider with a root domain');
@@ -84,11 +88,11 @@ test('service provider tests', async(t) => {
t.ok(result.length === 2 , 'successfully queried all service providers');
/* query one service providers */
result = await request.get(`/ServiceProviders/${sid}`, {
result = await request.get(`/ServiceProviders/${sid2}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved service provider by sid');
t.ok(result.name === 'johndoe' && result.root_domain === 'example.com', 'successfully retrieved service provider by sid');
/* update service providers */

View File

@@ -22,7 +22,8 @@ test('voip carrier tests', async(t) => {
auth: authAdmin,
json: true,
body: {
name: 'daveh'
name: 'daveh',
e164_leading_plus: true
}
});
t.ok(result.statusCode === 201, 'successfully created voip carrier');
@@ -33,7 +34,7 @@ test('voip carrier tests', async(t) => {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all voip carriers');
t.ok(result.length === 1 && result[0].e164_leading_plus, 'successfully queried all voip carriers');
/* query one voip carriers */
result = await request.get(`/VoipCarriers/${sid}`, {