Compare commits

...

24 Commits

Author SHA1 Message Date
Dave Horton
ea64fb1a58 add sms messaging support 2020-10-09 08:04:39 -04:00
Dave Horton
53763aae14 bugfix: createCall REST API to Teams endpoint was being blocked 2020-09-30 15:37:49 -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
Dave Horton
5d5bd223cd add script to create default service provider and account to database 2020-04-21 09:27:54 -04:00
Dave Horton
ed74ec45c2 minor swagger fix 2020-04-21 08:12:46 -04:00
Dave Horton
2fb2fc5c10 fix bug in choosing feature server for createCall 2020-04-21 08:08:55 -04:00
Dave Horton
e38137ae7b remove assert about env var for feature server location as we now get from db 2020-04-20 20:05:39 -04:00
Dave Horton
8e6b0ec111 bump deps 2020-04-20 12:14:13 -04:00
Dave Horton
be011db109 add support for sbc_addresses and ms_teams_tenants tables 2020-04-20 11:44:49 -04:00
Dave Horton
993833212f route createCall api requests among active freeswitch servers 2020-04-20 10:12:00 -04:00
Dave Horton
61a5ce2672 added basic login support for admin user 2020-04-01 16:27:18 -04:00
Dave Horton
c43d24b477 add script to initialize/reset admin password to default 2020-04-01 14:26:28 -04:00
Dave Horton
2accbcef74 remove application_sid from createCall payload if provided as null value 2020-03-24 08:07:39 -04:00
Dave Horton
7a144ffe74 support for login 2020-03-21 13:39:03 -04:00
37 changed files with 2170 additions and 276 deletions

View File

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

View File

@@ -16,7 +16,6 @@ JAMBONES_REDIS_HOST
JAMBONES_REDIS_PORT
JAMBONES_LOGLEVEL # defaults to info
JAMBONES_API_VERSION # defaults to v1
JAMBONES_CREATE_CALL_URL
HTTP_PORT # defaults to 3000
```

44
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();
@@ -15,21 +19,23 @@ 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_CREATE_CALL_URL, 'missing JAMBONES_CREATE_CALL_URL env var');
const {
retrieveCall,
deleteCall,
listCalls,
purgeCalls
} = require('jambonz-realtimedb-helpers')({
purgeCalls,
retrieveSet
} = 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,
@@ -47,18 +53,34 @@ Object.assign(app.locals, {
deleteCall,
listCalls,
purgeCalls,
retrieveSet,
lookupAppBySid,
lookupAccountBySid
lookupAccountBySid,
lookupAccountByPhoneNumber,
lookupAppByPhoneNumber
});
const unless = (paths, middleware) => {
return (req, res, next) => {
if (paths.find((path) => req.path.startsWith(path))) return next();
return middleware(req, res, next);
};
};
app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.urlencoded({
extended: true
}));
app.use(express.json());
app.use('/v1', 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

@@ -0,0 +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');

View File

@@ -0,0 +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');

View File

@@ -1,204 +1,262 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS = 0;
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `call_routes`;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS `lcr_carrier_set_entry`;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS `lcr_routes`;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS `phone_numbers`;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS `sip_gateways`;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS `voip_carriers`;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS `accounts`;
DROP TABLE IF EXISTS phone_numbers;
DROP TABLE IF EXISTS `applications`;
DROP TABLE IF EXISTS sip_gateways;
DROP TABLE IF EXISTS `service_providers`;
DROP TABLE IF EXISTS voip_carriers;
DROP TABLE IF EXISTS `webhooks`;
DROP TABLE IF EXISTS accounts;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS applications;
CREATE TABLE IF NOT EXISTS `call_routes`
DROP TABLE IF EXISTS service_providers;
DROP TABLE IF EXISTS webhooks;
CREATE TABLE call_routes
(
`call_route_sid` CHAR(36) NOT NULL UNIQUE ,
`priority` INTEGER NOT NULL,
`account_sid` CHAR(36) NOT NULL,
`regex` VARCHAR(255) NOT NULL,
`application_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`call_route_sid`)
call_route_sid CHAR(36) NOT NULL UNIQUE ,
priority INTEGER NOT NULL,
account_sid CHAR(36) NOT NULL,
regex VARCHAR(255) NOT NULL,
application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) ENGINE=InnoDB COMMENT='a regex-based pattern match for call routing';
CREATE TABLE IF NOT EXISTS `lcr_routes`
CREATE TABLE lcr_routes
(
`lcr_route_sid` CHAR(36),
`regex` VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
`description` VARCHAR(1024),
`priority` INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (`lcr_route_sid`)
lcr_route_sid CHAR(36),
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
CREATE TABLE IF NOT EXISTS `api_keys`
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),
PRIMARY KEY (`api_key_sid`)
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 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 IF NOT EXISTS `voip_carriers`
CREATE TABLE ms_teams_tenants
(
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE ,
`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',
PRIMARY KEY (`voip_carrier_sid`)
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 ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
service_provider_sid CHAR(36),
PRIMARY KEY (sbc_address_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,
PRIMARY KEY (user_sid)
);
CREATE TABLE voip_carriers
(
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
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';
CREATE TABLE IF NOT EXISTS `phone_numbers`
CREATE TABLE phone_numbers
(
`phone_number_sid` CHAR(36) UNIQUE ,
`number` VARCHAR(32) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL,
`account_sid` CHAR(36),
`application_sid` CHAR(36),
PRIMARY KEY (`phone_number_sid`)
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36) NOT NULL,
account_sid CHAR(36),
application_sid CHAR(36),
PRIMARY KEY (phone_number_sid)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE TABLE IF NOT EXISTS `webhooks`
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`)
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 IF NOT EXISTS `lcr_carrier_set_entry`
CREATE TABLE sip_gateways
(
`lcr_carrier_set_entry_sid` CHAR(36),
`workload` INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
`lcr_route_sid` CHAR(36) NOT NULL,
`voip_carrier_sid` CHAR(36) NOT NULL,
`priority` INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (`lcr_carrier_set_entry_sid`)
) COMMENT='An entry in the LCR routing list';
CREATE TABLE IF NOT EXISTS `sip_gateways`
(
`sip_gateway_sid` CHAR(36),
`ipv4` VARCHAR(32) 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',
`voip_carrier_sid` CHAR(36) NOT NULL,
`is_active` BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (`sip_gateway_sid`)
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.',
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',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
CREATE TABLE IF NOT EXISTS `applications`
CREATE TABLE lcr_carrier_set_entry
(
`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_status_hook_sid` CHAR(36) COMMENT 'webhook to call for call status events',
`speech_synthesis_vendor` VARCHAR(64) NOT NULL DEFAULT 'google',
`speech_synthesis_voice` VARCHAR(64) NOT NULL DEFAULT 'en-US-Wavenet-C',
`speech_recognizer_vendor` VARCHAR(64) NOT NULL DEFAULT 'google',
`speech_recognizer_language` VARCHAR(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (`application_sid`)
lcr_carrier_set_entry_sid CHAR(36),
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
lcr_route_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (lcr_carrier_set_entry_sid)
) COMMENT='An entry in the LCR routing list';
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 ',
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),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (application_sid)
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls ';
CREATE TABLE IF NOT EXISTS `service_providers`
CREATE TABLE service_providers
(
`service_provider_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL UNIQUE ,
`description` VARCHAR(255),
`root_domain` VARCHAR(128) UNIQUE ,
`registration_hook_sid` CHAR(36),
PRIMARY KEY (`service_provider_sid`)
service_provider_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL UNIQUE ,
description VARCHAR(255),
root_domain VARCHAR(128) UNIQUE ,
registration_hook_sid CHAR(36),
ms_teams_fqdn VARCHAR(255),
PRIMARY KEY (service_provider_sid)
) ENGINE=InnoDB COMMENT='A partition of the platform used by one service provider';
CREATE TABLE IF NOT EXISTS `accounts`
CREATE TABLE accounts
(
`account_sid` CHAR(36) NOT NULL UNIQUE ,
`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',
`device_calling_application_sid` CHAR(36) COMMENT 'application to use for outbound calling from an account',
`is_active` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`account_sid`)
account_sid CHAR(36) NOT NULL UNIQUE ,
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',
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (account_sid)
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services';
CREATE INDEX `call_routes_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`);
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 application_sid_idxfk (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
CREATE INDEX `api_keys_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`);
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);
CREATE INDEX `api_keys_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 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 `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `voip_carriers_name_idx` ON `voip_carriers` (`name`);
ALTER TABLE `voip_carriers` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_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 `voip_carriers` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
CREATE INDEX `phone_numbers_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`);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
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);
CREATE INDEX `webhooks_webhook_sid_idx` ON `webhooks` (`webhook_sid`);
ALTER TABLE `lcr_carrier_set_entry` ADD FOREIGN KEY lcr_route_sid_idxfk (`lcr_route_sid`) REFERENCES `lcr_routes` (`lcr_route_sid`);
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX name_idx ON users (name);
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);
ALTER TABLE `lcr_carrier_set_entry` ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE UNIQUE INDEX `sip_gateways_sip_gateway_idx_hostport` ON `sip_gateways` (`ipv4`,`port`);
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);
ALTER TABLE `sip_gateways` ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (`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 UNIQUE INDEX `applications_idx_name` ON `applications` (`account_sid`,`name`);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX `applications_application_sid_idx` ON `applications` (`application_sid`);
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
CREATE INDEX `applications_account_sid_idx` ON `applications` (`account_sid`);
ALTER TABLE `applications` ADD FOREIGN KEY account_sid_idxfk_4 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
ALTER TABLE `applications` ADD FOREIGN KEY call_hook_sid_idxfk (`call_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE `applications` ADD FOREIGN KEY call_status_hook_sid_idxfk (`call_status_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `service_providers_root_domain_idx` ON `service_providers` (`root_domain`);
ALTER TABLE `service_providers` ADD FOREIGN KEY registration_hook_sid_idxfk (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`);
CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
CREATE INDEX `accounts_sip_realm_idx` ON `accounts` (`sip_realm`);
CREATE INDEX `accounts_service_provider_sid_idx` ON `accounts` (`service_provider_sid`);
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
ALTER TABLE `accounts` ADD FOREIGN KEY registration_hook_sid_idxfk_1 (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
CREATE INDEX application_sid_idx ON applications (application_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 `accounts` ADD FOREIGN KEY device_calling_application_sid_idxfk (`device_calling_application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
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);
ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (registration_hook_sid) REFERENCES webhooks (webhook_sid);
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 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

@@ -1,5 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<SQLContainer>
<SQLTable>
<name><![CDATA[users]]></name>
<schema><![CDATA[]]></schema>
<location>
<x>1599.00</x>
<y>37.00</y>
</location>
<size>
<width>250.00</width>
<height>120.00</height>
</size>
<zorder>11</zorder>
<SQLField>
<name><![CDATA[user_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[F33F9604-ADC9-46E3-AE51-A2A839A81758]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[name]]></name>
<type><![CDATA[CHAR(36)]]></type>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[6E8DE796-46DA-4644-AA7F-1A75D9B834FB]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[hashed_password]]></name>
<type><![CDATA[VARCHAR(1024)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[3E7E38A2-96FC-4E28-BF10-332823FB17F1]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[salt]]></name>
<type><![CDATA[CHAR(16)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[F21A170F-5922-4749-A062-6C7516051560]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[force_change]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[TRUE]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[57655C0C-9427-4CC7-9502-24ACF56AAECF]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[2A735FAB-592C-42E5-9C8B-06B109314799]]></uid>
</SQLTable>
<SQLTable>
<name><![CDATA[voip_carriers]]></name>
<schema><![CDATA[]]></schema>
@@ -11,7 +63,7 @@
</location>
<size>
<width>266.00</width>
<height>120.00</height>
<height>140.00</height>
</size>
<zorder>6</zorder>
<SQLField>
@@ -67,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[5]]></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>
@@ -78,12 +137,12 @@
<comment><![CDATA[An authorization token that is used to access the REST api]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>1279.00</x>
<y>61.00</y>
<x>1319.00</x>
<y>38.00</y>
</location>
<size>
<width>252.00</width>
<height>100.00</height>
<width>245.00</width>
<height>160.00</height>
</size>
<zorder>1</zorder>
<SQLField>
@@ -132,7 +191,25 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[3F553F20-AA47-471E-A650-0B4CEC9DCB0A]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<SQLField>
<name><![CDATA[expires_at]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NULL]]></defaultValue>
<uid><![CDATA[DE86BC18-858E-4D7E-9B83-891DB2861434]]></uid>
</SQLField>
<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>
@@ -142,8 +219,8 @@
<schema><![CDATA[]]></schema>
<comment><![CDATA[An HTTP callback]]></comment>
<location>
<x>1383.00</x>
<y>365.00</y>
<x>1315.00</x>
<y>376.00</y>
</location>
<size>
<width>254.00</width>
@@ -183,7 +260,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[04BB457A-D532-4780-8A58-5900094171EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[1]]></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>
@@ -194,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>
@@ -254,11 +331,89 @@
<uid><![CDATA[9B4208B5-9E3B-4B76-B7F7-4E5D36B99BF2]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[9]]></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>
</SQLTable>
<SQLTable>
<name><![CDATA[ms_teams_tenants]]></name>
<schema><![CDATA[]]></schema>
<comment><![CDATA[A Microsoft Teams customer tenant]]></comment>
<location>
<x>1309.00</x>
<y>219.00</y>
</location>
<size>
<width>298.00</width>
<height>120.00</height>
</size>
<zorder>12</zorder>
<SQLField>
<name><![CDATA[ms_teams_tenant_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[A0E48CB5-C2B2-47FC-8486-EF5F16DAF797]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[service_provider_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>service_provider_sid</referencesField>
<referencesTable>service_providers</referencesTable>
<referencesField><![CDATA[service_provider_sid]]></referencesField>
<referencesTable><![CDATA[service_providers]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[58E1702C-6A95-4B17-8C08-8A3810EA16A1]]></referencesFieldUID>
<referencesTableUID><![CDATA[F294B51E-F867-47CA-BC1F-F70BDF8170FF]]></referencesTableUID>
<indexed><![CDATA[0]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[519A33A0-B678-4F60-892F-ADA90E656500]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[account_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>account_sid</referencesField>
<referencesTable>accounts</referencesTable>
<referencesField><![CDATA[account_sid]]></referencesField>
<referencesTable><![CDATA[accounts]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<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>
<name><![CDATA[application_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>application_sid</referencesField>
<referencesTable>applications</referencesTable>
<referencesField><![CDATA[application_sid]]></referencesField>
<referencesTable><![CDATA[applications]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>2</destinationCardinality>
<referencesFieldUID><![CDATA[EF943D13-DCB0-43C1-B03F-550612E20F9D]]></referencesFieldUID>
<referencesTableUID><![CDATA[E97EE4F0-7ED7-4E8C-862E-D98192D6EAE0]]></referencesTableUID>
<forcedUnique><![CDATA[0]]></forcedUnique>
<uid><![CDATA[7959F455-D49D-4185-97B5-37C7FAAB339B]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[tenant_fqdn]]></name>
<type><![CDATA[VARCHAR(255)]]></type>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<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>
<name><![CDATA[lcr_carrier_set_entry]]></name>
<schema><![CDATA[]]></schema>
@@ -323,7 +478,7 @@
<objectComment><![CDATA[lower priority carriers are attempted first]]></objectComment>
<uid><![CDATA[01F61C68-799B-49B0-9E6A-0E2162EE5A54]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[4]]></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>
@@ -356,7 +511,7 @@
<SQLField>
<name><![CDATA[name]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<indexed><![CDATA[1]]></indexed>
<indexed><![CDATA[0]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[87414407-17CB-4582-9C0B-73CA548E1016]]></uid>
</SQLField>
@@ -421,7 +576,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[C7130A90-DBB4-424D-A9A9-CB203C32350C]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[8]]></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>
@@ -503,7 +658,7 @@
<uid><![CDATA[962CB80A-54CB-4C6A-9591-9BFC644CF80F]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[7]]></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>
@@ -529,7 +684,7 @@
</SQLField>
<SQLField>
<name><![CDATA[ipv4]]></name>
<type><![CDATA[VARCHAR(32)]]></type>
<type><![CDATA[VARCHAR(128)]]></type>
<notNull><![CDATA[1]]></notNull>
<objectComment><![CDATA[ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.]]></objectComment>
<uid><![CDATA[F18DB7D4-F902-4863-870C-CB07032AE17C]]></uid>
@@ -595,7 +750,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[1C744DE3-39BD-4EC6-B427-7EB2DD258771]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[3]]></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>
@@ -611,7 +766,7 @@
</location>
<size>
<width>345.00</width>
<height>220.00</height>
<height>260.00</height>
</size>
<zorder>0</zorder>
<SQLField>
@@ -627,7 +782,7 @@
<SQLField>
<name><![CDATA[name]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<indexed><![CDATA[1]]></indexed>
<indexed><![CDATA[0]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[E0EDB7B1-B7F7-4F56-B94F-6B81BB87C514]]></uid>
</SQLField>
@@ -659,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>
@@ -676,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>
@@ -683,11 +852,17 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[05D3C937-255D-44A5-A102-40303FA37CF1]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_synthesis_language]]></name>
<type><![CDATA[VARCHAR(12)]]></type>
<defaultValue><![CDATA[en-US]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[2498FC63-58A1-40AF-8502-ABC4F1F5F541]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_synthesis_voice]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<defaultValue><![CDATA[en-US-Wavenet-C]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[929D66F0-64B9-4D7C-AB4B-24F131E1178F]]></uid>
</SQLField>
<SQLField>
@@ -722,11 +897,80 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[3FDDDF3B-375D-4DE4-B759-514438845F7D]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[6]]></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>
</SQLTable>
<SQLTable>
<name><![CDATA[sbc_addresses]]></name>
<schema><![CDATA[]]></schema>
<location>
<x>1305.00</x>
<y>564.00</y>
</location>
<size>
<width>281.00</width>
<height>120.00</height>
</size>
<zorder>13</zorder>
<SQLField>
<name><![CDATA[sbc_address_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[468F8C06-5A38-494A-8E77-3F53F111237B]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[ipv4]]></name>
<type><![CDATA[VARCHAR(255)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[4FE97DA7-8D8E-4BE3-982A-01B0444FB070]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[port]]></name>
<type><![CDATA[INTEGER]]></type>
<defaultValue><![CDATA[5060]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[707EF28F-F4AF-4B7E-AB04-C128C894ECE9]]></uid>
</SQLField>
<SQLIndex>
<name><![CDATA[sbc_addresses_idx_host_port]]></name>
<fieldName><![CDATA[ipv4]]></fieldName>
<fieldName><![CDATA[port]]></fieldName>
<SQLIndexEntry>
<name><![CDATA[ipv4]]></name>
<prefixSize><![CDATA[]]></prefixSize>
<fieldUid><![CDATA[4FE97DA7-8D8E-4BE3-982A-01B0444FB070]]></fieldUid>
</SQLIndexEntry>
<SQLIndexEntry>
<name><![CDATA[port]]></name>
<prefixSize><![CDATA[]]></prefixSize>
<fieldUid><![CDATA[707EF28F-F4AF-4B7E-AB04-C128C894ECE9]]></fieldUid>
</SQLIndexEntry>
<indexNamePrefix><![CDATA[sbc_addresses]]></indexNamePrefix>
<uid><![CDATA[174BD35A-B09C-4F4A-B3D2-D5464D927D9B]]></uid>
</SQLIndex>
<SQLField>
<name><![CDATA[service_provider_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>service_provider_sid</referencesField>
<referencesTable>service_providers</referencesTable>
<referencesField><![CDATA[service_provider_sid]]></referencesField>
<referencesTable><![CDATA[service_providers]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[58E1702C-6A95-4B17-8C08-8A3810EA16A1]]></referencesFieldUID>
<referencesTableUID><![CDATA[F294B51E-F867-47CA-BC1F-F70BDF8170FF]]></referencesTableUID>
<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>
<name><![CDATA[lcr_routes]]></name>
<schema><![CDATA[]]></schema>
@@ -768,7 +1012,7 @@
<uid><![CDATA[B73773BA-AB1B-47AA-B995-2D2FE006198F]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[2]]></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>
@@ -779,12 +1023,12 @@
<comment><![CDATA[A partition of the platform used by one service provider]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>813.00</x>
<y>99.00</y>
<x>838.00</x>
<y>96.00</y>
</location>
<size>
<width>293.00</width>
<height>120.00</height>
<height>140.00</height>
</size>
<zorder>3</zorder>
<SQLField>
@@ -831,6 +1075,11 @@
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
<uid><![CDATA[506BBE72-1A97-4776-B2C3-D94169652FFE]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[ms_teams_fqdn]]></name>
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[FA39B463-61C7-4654-BE9C-D1AC39AB1B97]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[0]]></labelWindowIndex>
<objectComment><![CDATA[A partition of the platform used by one service provider]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
@@ -850,17 +1099,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[2146.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[1322.000000]]></windowHeight>
<windowLocationX><![CDATA[2553.000000]]></windowLocationX>
<windowLocationY><![CDATA[95.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 0}]]></windowScrollOrigin>
<windowWidth><![CDATA[2423.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>

71
db/reset_admin_password.js Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env node
const {getMysqlConnection} = require('../lib/db');
const crypto = require('crypto');
const uuidv4 = require('uuid/v4');
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
* @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, [
sid,
'admin',
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);
});
});
});
});

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',
}
];

55
lib/models/sbc.js Normal file
View File

@@ -0,0 +1,55 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
class Sbc 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 sbc_addresses WHERE service_provider_sid = ?' :
'SELECT * from sbc_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);
});
});
});
}
}
Sbc.table = 'sbc_addresses';
Sbc.fields = [
{
name: 'sbc_address_sid',
type: 'string',
primaryKey: true
},
{
name: 'ipv4',
type: 'string',
required: true
},
{
name: 'port',
type: 'number'
},
{
name: 'service_provider_sid',
type: 'string'
}
];
module.exports = Sbc;

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',
}

37
lib/models/tenant.js Normal file
View File

@@ -0,0 +1,37 @@
const Model = require('./model');
class MsTeamsTenant extends Model {
constructor() {
super();
}
}
MsTeamsTenant.table = 'ms_teams_tenants';
MsTeamsTenant.fields = [
{
name: 'ms_teams_tenant_sid',
type: 'string',
primaryKey: true
},
{
name: 'service_provider_sid',
type: 'string',
required: true
},
{
name: 'account_sid',
type: 'string',
required: true
},
{
name: 'application_sid',
type: 'string'
},
{
name: 'tenant_fqdn',
type: 'string',
required: true
}
];
module.exports = MsTeamsTenant;

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');
@@ -12,6 +14,7 @@ const preconditions = {
'update': validateUpdate,
'delete': validateDelete
};
let idx = 0;
function coerceNumbers(callInfo) {
if (Array.isArray(callInfo)) {
@@ -26,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;
@@ -64,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':
@@ -90,11 +103,11 @@ async function validateCreateCall(logger, sid, req) {
try {
logger.debug(`Accounts:validateCreateCall retrieving application ${obj.application_sid}`);
const application = await lookupAppBySid(obj.application_sid);
logger.debug(`Accounts:validateCreateCall retrieved application ${JSON.stringify(application)}`);
Object.assign(obj, {
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
@@ -106,9 +119,13 @@ async function validateCreateCall(logger, sid, req) {
}
}
else {
delete obj.application_sid;
// TODO: these should be retrieved from account, using account_sid if provided
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'
});
@@ -119,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) {
@@ -248,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);
}
@@ -258,13 +312,22 @@ router.put('/:sid', async(req, res) => {
*/
router.post('/:sid/Calls', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
try {
const serviceUrl = process.env.JAMBONES_CREATE_CALL_URL;
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(500);
}
const ip = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
const serviceUrl = `http://${ip}:3000/v1/createCall`;
await validateCreateCall(logger, sid, req);
logger.debug({payload: req.body}, `sending POST to ${serviceUrl}`);
logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
@@ -272,11 +335,11 @@ router.post('/:sid/Calls', async(req, res) => {
body: req.body
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
logger.error(err, `Error sending createCall POST to ${ip}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${ip}`);
return res.sendStatus(500);
}
res.status(201).json(body);
@@ -297,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);
}
@@ -320,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);
}
@@ -343,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);
}
@@ -373,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

@@ -11,7 +11,7 @@ const decorators = {
'delete': remove
};
function decorate(router, klass, methods, preconditions) {
function decorate(router, klass, methods, preconditions = {}) {
const decs = methods && Array.isArray(methods) && methods[0] !== '*' ? methods : Object.keys(decorators);
decs.forEach((m) => {
assert(m in decorators);

View File

@@ -15,5 +15,13 @@ api.use('/PhoneNumbers', isAdminScope, 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('/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;

75
lib/routes/api/login.js Normal file
View File

@@ -0,0 +1,75 @@
const router = require('express').Router();
const crypto = require('crypto');
const {getMysqlConnection} = require('../../db');
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;
const {username, password} = req.body;
if (!username || !password) {
logger.info('Bad POST to /login is missing username or password');
return res.sendStatus(400);
}
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
conn.query(retrieveSql, [username], (err, results) => {
conn.release();
if (err) {
logger.error({err}, 'Error getting db connection');
return res.sendStatus(500);
}
if (0 === results.length) {
logger.info(`Failed login attempt for user ${username}`);
return res.sendStatus(403);
}
logger.info({results}, 'successfully retrieved account');
const salt = results[0].salt;
const trueHash = results[0].hashed_password;
const forceChange = results[0].force_change;
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});
getMysqlConnection((err, conn) => {
if (err) {
logger.error({err}, 'Error getting db connection');
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});
});
});
});
});
});
module.exports = router;

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');

19
lib/routes/api/sbcs.js Normal file
View File

@@ -0,0 +1,19 @@
const router = require('express').Router();
const Sbc = require('../../models/sbc');
const decorate = require('./decorate');
const sysError = require('./error');
decorate(router, Sbc, ['add', 'delete']);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Sbc.retrieveAll(req.query.service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

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

@@ -0,0 +1,8 @@
const router = require('express').Router();
const Tenant = require('../../models/tenant');
const decorate = require('./decorate');
const preconditions = {};
decorate(router, Tenant, ['*'], preconditions);
module.exports = router;

96
lib/routes/api/users.js Normal file
View File

@@ -0,0 +1,96 @@
const router = require('express').Router();
const crypto = require('crypto');
const {getMysqlConnection} = require('../../db');
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 genRandomString = (len) => {
return crypto.randomBytes(Math.ceil(len / 2))
.toString('hex') /** convert to hexadecimal format */
.slice(0, len); /** return required number of characters */
};
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);
};
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);
}
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);
}
logger.info({results}, 'successfully retrieved user');
const old_salt = results[0].salt;
const old_hashed_password = results[0].hashed_password;
const {passwordHash} = sha512(old_password, old_salt);
if (old_hashed_password !== passwordHash) return res.sendStatus(403);
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});
});
});
});
});
});
});
module.exports = router;

View File

@@ -12,6 +12,92 @@ servers:
- url: /v1
description: development server
paths:
/Sbcs:
post:
summary: add an SBC address
operationId: createSbc
requestBody:
content:
application/json:
schema:
type: object
properties:
ipv4:
type: string
port:
type: number
service_provider_sid:
type: string
description: service provider scope for the generated api key
required:
- ipv4
responses:
201:
description: sbc address successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve public IP addresses of the jambonz Sbcs
operationId: listSbcs
parameters:
- in: query
name: service_provider_sid
required: false
schema:
type: string
description: return only the SBCs operated for the sole use of this service provider
responses:
200:
description: list of SBC addresses
content:
application/json:
schema:
type: array
items:
properties:
ipv4:
type: string
description: ip address of one of our Sbcs
required:
- ipv4
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Sbcs/{SbcSid}:
parameters:
- name: SbcSid
in: path
required: true
schema:
type: string
delete:
summary: delete sbc address
operationId: deleteSbcAddress
responses:
200:
description: sbc address deleted
404:
description: sbc address not found
/ApiKeys:
post:
summary: create an api key
@@ -28,6 +114,9 @@ paths:
account_sid:
type: string
description: account scope for the generated api key
expiry_secs:
type: number
description: duration of key validity, in seconds
responses:
201:
description: api key successfully created
@@ -64,6 +153,89 @@ paths:
404:
description: api key or account not found
/login:
post:
summary: login a user and receive an api token
operationId: loginUser
requestBody:
content:
application/json:
schema:
type: object
properties:
username:
type: string
description: login username
password:
type: string
description: login password
required:
- username
- password
responses:
200:
description: login succeeded
content:
application/json:
schema: '#/components/schemas/Login'
403:
description: login failed
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users/{UserSid}:
parameters:
- name: UserSid
in: path
required: true
style: simple
explode: false
schema:
type: string
put:
summary: update a user password
operationId: updateUser
requestBody:
content:
application/json:
schema:
type: object
properties:
old_password:
type: string
description: existing password, which is to be replaced
new_password:
type: string
description: new password
required:
- old_password
- new_password
responses:
200:
description: password successfully changed
content:
application/json:
schema:
$ref: '#/components/schemas/Login'
403:
description: password change failed
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/VoipCarriers:
post:
summary: create voip carrier
@@ -79,6 +251,8 @@ paths:
description: voip carrier name
description:
type: string
e164_leading_plus:
type: boolean
required:
- name
responses:
@@ -516,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:
@@ -636,6 +806,140 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
/MicrosoftTeamsTenants:
post:
summary: provision a customer tenant for MS Teams
operationId: createMsTeamsTenant
requestBody:
content:
application/json:
schema:
type: object
properties:
service_provider_sid:
type: string
format: uuid
example: 85f9c036-ba61-4f28-b2f5-617c23fa68ff
account_sid:
type: string
format: uuid
example: 85f9c036-ba61-4f28-b2f5-617c23fa68ff
application_sid:
type: string
format: uuid
example: 85f9c036-ba61-4f28-b2f5-617c23fa68ff
tenant_fqdn:
type: string
example: customer.contoso.com
required:
- service_provider_sid
- account
- tenant_fqdn
responses:
201:
description: tenant successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: list MS Teams tenants
operationId: listMsTeamsTenants
responses:
200:
description: list of tenants
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/MsTeamsTenant'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/MicrosoftTeamsTenants/{TenantSid}:
parameters:
- name: TenantSid
in: path
required: true
schema:
type: string
format: uuid
delete:
summary: delete an MS Teams tenant
operationId: deleteTenant
responses:
204:
description: tenant successfully deleted
404:
description: tenant not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve an MS Teams tenant
operationId: getTenant
responses:
200:
description: tenant found
content:
application/json:
schema:
$ref: '#/components/schemas/MsTeamsTenant'
404:
description: account not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
summary: update tenant
operationId: updateAccount
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tenant'
responses:
204:
description: tenant updated
404:
description: tenant not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts:
post:
summary: create an account
@@ -898,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:
@@ -917,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:
@@ -1082,7 +1417,7 @@ paths:
from:
type: string
description: The calling party number
example: 16172375089
example: "16172375089"
timeout:
type: integer
description: the number of seconds to wait for call to be answered. Defaults to 60.
@@ -1101,9 +1436,9 @@ paths:
application/json:
schema:
required:
- callSid
- sid
properties:
callSid:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
@@ -1133,8 +1468,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Calls/{CallSid}:
parameters:
- name: AccountSid
@@ -1200,8 +1533,6 @@ paths:
properties:
call_hook:
$ref: '#/components/schemas/Webhook'
call_status_hook:
$ref: '#/components/schemas/Webhook'
call_status:
type: string
enum:
@@ -1212,6 +1543,11 @@ paths:
enum:
- pause
- resume
mute_status:
type: string
enum:
- mute
- unmute
whisper:
$ref: '#/components/schemas/Webhook'
responses:
@@ -1232,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:
@@ -1240,6 +1608,17 @@ components:
scheme: bearer
bearerFormat: token
schemas:
Login:
type: object
properties:
user_sid:
type: string
api_token:
type: string
change_password:
type: boolean
required:
- user_sid
SuccessfulApiKeyAdd:
type: object
required:
@@ -1283,12 +1662,11 @@ components:
type: string
root_domain:
type: string
hook_basic_auth_user:
registration_hook:
$ref: '#/components/schemas/Webhook'
description: authentication webhook for registration
ms_teams_fqdn:
type: string
format: url
hook_basic_auth_password:
type: string
format: url
required:
- service_provider_sid
- name
@@ -1302,6 +1680,8 @@ components:
type: string
description:
type: string
e164_leading_plus:
type: boolean
required:
- voip_carrier_sid
- name
@@ -1368,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:
@@ -1384,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:
@@ -1444,6 +1849,23 @@ components:
required:
- url
example: {"url": "https://acme.com", "method": "POST"}
MsTeamsTenant:
type: object
properties:
service_provider_sid:
type: string
format: uuid
account_sid:
type: string
format: uuid
application_sid:
type: string
format: uuid
tenant_fqdn:
type: string
required:
- service_provider_sid
- tenant_fqdn
Call:
type: object
properties:
@@ -1526,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.4",
"version": "1.2.0",
"description": "",
"main": "app.js",
"scripts": {
@@ -15,28 +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.2",
"jambonz-realtimedb-helpers": "0.1.7",
"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",
"tape": "^4.11.0",
"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.7.2",
"eslint": "^7.10.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^14.1.1",
"request-promise-native": "^1.0.8",
"tap": "^14.10.2",
"nyc": "^15.1.0",
"request-promise-native": "^1.0.9",
"tap-dot": "^2.0.0",
"tap-spec": "^5.0.0"
"tap-spec": "^5.0.0",
"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

@@ -6,4 +6,6 @@ require('./accounts');
require('./phone-numbers');
require('./applications');
require('./auth');
require('./sbcs');
require('./ms-teams');
require('./remove-test-db');

89
test/ms-teams.js Normal file
View File

@@ -0,0 +1,89 @@
const test = require('tape').test ;
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 {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('sbc_addresses tests', async(t) => {
const app = require('../app');
let sid;
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', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
service_provider_sid,
account_sid,
tenant_fqdn: 'foo.bar.baz'
}
});
t.ok(result.statusCode === 201, 'successfully added ms teams tenant');
const sid1 = result.body.sid;
/* add a second tenant */
result = await request.post('/MicrosoftTeamsTenants', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
service_provider_sid,
account_sid: account_sid2,
tenant_fqdn: 'junk.bar.baz'
}
});
t.ok(result.statusCode === 201, 'successfully added ms teams tenant');
const sid2 = result.body.sid;
result = await request.get('/MicrosoftTeamsTenants', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true
});
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();
}
catch (err) {
console.error(err);
t.end(err);
}
});

70
test/sbcs.js Normal file
View File

@@ -0,0 +1,70 @@
const test = require('tape').test ;
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 {createServiceProvider, deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('sbc_addresses tests', async(t) => {
const app = require('../app');
let sid;
try {
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,
auth: authAdmin,
json: true,
body: {
ipv4: '192.168.1.4',
service_provider_sid
}
});
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');
result = await request.get(`/Sbcs?service_provider_sid=${service_provider_sid}`, {
resolveWithFullResponse: true,
auth: authAdmin,
json: true
});
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, '/ServiceProviders', service_provider_sid);
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

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}`, {