mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
26 Commits
messaging
...
v0.5-branc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08809c14e9 | ||
|
|
bf557ba252 | ||
|
|
af79d92e71 | ||
|
|
1f4b175fc8 | ||
|
|
ab7c69c0e8 | ||
|
|
fc61d3d2fa | ||
|
|
081a83e121 | ||
|
|
7e6261eec8 | ||
|
|
5f10ef585f | ||
|
|
c9eeb41eb6 | ||
|
|
843e1e4e80 | ||
|
|
fb86875576 | ||
|
|
e633de5d4a | ||
|
|
d8ac0a7aa2 | ||
|
|
0da3bf94a6 | ||
|
|
4e9b079f0d | ||
|
|
7876b0efa6 | ||
|
|
dd53a62457 | ||
|
|
09928597e0 | ||
|
|
484fa7841a | ||
|
|
c578757dd2 | ||
|
|
6b01f7f07e | ||
|
|
93ddaf86d2 | ||
|
|
6e0fc76281 | ||
|
|
ea64fb1a58 | ||
|
|
53763aae14 |
@@ -8,7 +8,7 @@
|
||||
"jsx": false,
|
||||
"modules": false
|
||||
},
|
||||
"ecmaVersion": 2017
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"plugins": ["promise"],
|
||||
"rules": {
|
||||
|
||||
19
.github/workflows/npm-publish.yml
vendored
Normal file
19
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- run: npm test
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
dist: bionic
|
||||
language: node_js
|
||||
node_js:
|
||||
- "lts/*"
|
||||
services:
|
||||
- mysql
|
||||
script:
|
||||
- npm test
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,13 +1,16 @@
|
||||
FROM node:lts-alpine
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python make g++
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
FROM node:alpine as app
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
COPY package.json /usr/src/app/
|
||||
RUN npm install
|
||||
COPY . /usr/src/app
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# jambones-api-server [](http://travis-ci.org/jambonz/jambones-api-server)
|
||||
# jambonz-api-server 
|
||||
|
||||
Jambones REST API server.
|
||||
|
||||
|
||||
33
app.js
33
app.js
@@ -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();
|
||||
@@ -20,6 +24,8 @@ const {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
pushBack,
|
||||
listQueues,
|
||||
purgeCalls,
|
||||
retrieveSet
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
@@ -28,10 +34,13 @@ const {
|
||||
}, logger);
|
||||
const {
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
lookupAppByPhoneNumber
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
@@ -46,10 +55,14 @@ Object.assign(app.locals, {
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
pushBack,
|
||||
listQueues,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
lookupAppByPhoneNumber
|
||||
});
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
@@ -60,13 +73,19 @@ const unless = (paths, middleware) => {
|
||||
};
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use('/v1', unless(['/login', '/Users'], passport.authenticate('bearer', { session: false })));
|
||||
app.use('/v1', unless(['/login', '/Users', '/messaging', '/outboundSMS'], passport.authenticate('bearer', {
|
||||
session: false
|
||||
})));
|
||||
app.use('/', routes);
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err, 'burped error');
|
||||
res.status(err.status || 500).json({msg: err.message});
|
||||
res.status(err.status || 500).json({
|
||||
msg: err.message
|
||||
});
|
||||
});
|
||||
logger.info(`listening for HTTP traffic on port ${PORT}`);
|
||||
app.listen(PORT);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
create database jambones_test;
|
||||
create user jambones_test@localhost IDENTIFIED WITH mysql_native_password by 'jambones_test';
|
||||
grant all on jambones_test.* to jambones_test@localhost;
|
||||
create user jambones_test@'%' IDENTIFIED WITH mysql_native_password by 'jambones_test';
|
||||
grant all on jambones_test.* to jambones_test@'%';
|
||||
|
||||
@@ -22,10 +22,10 @@ DROP TABLE IF EXISTS sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS voip_carriers;
|
||||
|
||||
DROP TABLE IF EXISTS accounts;
|
||||
|
||||
DROP TABLE IF EXISTS applications;
|
||||
|
||||
DROP TABLE IF EXISTS accounts;
|
||||
|
||||
DROP TABLE IF EXISTS service_providers;
|
||||
|
||||
DROP TABLE IF EXISTS webhooks;
|
||||
@@ -38,7 +38,7 @@ 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';
|
||||
) COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
@@ -55,11 +55,11 @@ 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,
|
||||
expires_at TIMESTAMP NULL DEFAULT NULL,
|
||||
last_used TIMESTAMP NULL DEFAULT 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';
|
||||
) COMMENT='An authorization token that is used to access the REST api';
|
||||
|
||||
CREATE TABLE ms_teams_tenants
|
||||
(
|
||||
@@ -98,8 +98,15 @@ 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,
|
||||
requires_register BOOLEAN NOT NULL DEFAULT false,
|
||||
register_username VARCHAR(64),
|
||||
register_sip_realm VARCHAR(64),
|
||||
register_password VARCHAR(64),
|
||||
tech_prefix VARCHAR(16),
|
||||
diversion VARCHAR(32),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
@@ -143,20 +150,36 @@ priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempt
|
||||
PRIMARY KEY (lcr_carrier_set_entry_sid)
|
||||
) COMMENT='An entry in the LCR routing list';
|
||||
|
||||
CREATE TABLE 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',
|
||||
queue_event_hook_sid CHAR(36) COMMENT 'webhook to call when members enter or leave a queue created by this account.',
|
||||
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
webhook_secret VARCHAR(36),
|
||||
disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
CREATE TABLE applications
|
||||
(
|
||||
application_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
|
||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account',
|
||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
||||
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
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 ';
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
CREATE TABLE service_providers
|
||||
(
|
||||
@@ -167,19 +190,7 @@ 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 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)
|
||||
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services';
|
||||
) COMMENT='A partition of the platform used by one service provider';
|
||||
|
||||
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);
|
||||
@@ -232,6 +243,17 @@ ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route
|
||||
|
||||
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 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 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
|
||||
|
||||
CREATE INDEX application_sid_idx ON applications (application_sid);
|
||||
@@ -242,18 +264,11 @@ ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REF
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
|
||||
CREATE INDEX name_idx ON service_providers (name);
|
||||
CREATE INDEX root_domain_idx ON service_providers (root_domain);
|
||||
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);
|
||||
ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
124
db/jambones.sqs
124
db/jambones.sqs
@@ -56,14 +56,13 @@
|
||||
<name><![CDATA[voip_carriers]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>417.00</x>
|
||||
<y>263.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>266.00</width>
|
||||
<height>140.00</height>
|
||||
<height>280.00</height>
|
||||
</size>
|
||||
<zorder>6</zorder>
|
||||
<SQLField>
|
||||
@@ -126,6 +125,45 @@
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[123EA4AC-627B-42A1-8779-D72494E8D47F]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[requires_register]]></name>
|
||||
<type><![CDATA[BOOLEAN]]></type>
|
||||
<defaultValue><![CDATA[false]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[B694DA3E-F58D-44C5-980F-E0CFBE6DFA02]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[register_username]]></name>
|
||||
<type><![CDATA[VARCHAR(64)]]></type>
|
||||
<uid><![CDATA[7EA13180-1746-44F5-8699-6099D5D29018]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[register_sip_realm]]></name>
|
||||
<type><![CDATA[VARCHAR(64)]]></type>
|
||||
<uid><![CDATA[163F2E47-6536-4A30-BD0A-4BBAA5AB4214]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[register_password]]></name>
|
||||
<type><![CDATA[VARCHAR(64)]]></type>
|
||||
<uid><![CDATA[3699DD5F-20F9-4650-86EB-A08A90894C59]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[tech_prefix]]></name>
|
||||
<type><![CDATA[VARCHAR(16)]]></type>
|
||||
<uid><![CDATA[58305E16-A895-4E7B-866F-F2A7BAD8B609]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[diversion]]></name>
|
||||
<type><![CDATA[VARCHAR(32)]]></type>
|
||||
<uid><![CDATA[33E3BA51-A9A6-40D2-BAF8-F8E67CC9DD13]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[is_active]]></name>
|
||||
<type><![CDATA[BOOLEAN]]></type>
|
||||
<defaultValue><![CDATA[true]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[8A6DFB34-1620-4DA6-AB6A-FFA79F71D110]]></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>
|
||||
@@ -135,7 +173,6 @@
|
||||
<name><![CDATA[api_keys]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[An authorization token that is used to access the REST api]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>1319.00</x>
|
||||
<y>38.00</y>
|
||||
@@ -194,11 +231,13 @@
|
||||
<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>
|
||||
@@ -221,7 +260,7 @@
|
||||
<y>376.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>254.00</width>
|
||||
<width>268.00</width>
|
||||
<height>120.00</height>
|
||||
</size>
|
||||
<zorder>10</zorder>
|
||||
@@ -267,10 +306,9 @@
|
||||
<name><![CDATA[call_routes]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[a regex-based pattern match for call routing]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>424.00</x>
|
||||
<y>461.00</y>
|
||||
<x>407.00</x>
|
||||
<y>584.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>254.00</width>
|
||||
@@ -485,14 +523,13 @@
|
||||
<name><![CDATA[accounts]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[An enterprise that uses the platform for comm services]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>825.00</x>
|
||||
<y>321.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>380.00</width>
|
||||
<height>160.00</height>
|
||||
<height>220.00</height>
|
||||
</size>
|
||||
<zorder>4</zorder>
|
||||
<SQLField>
|
||||
@@ -546,13 +583,29 @@
|
||||
<referencesTable>webhooks</referencesTable>
|
||||
<referencesField><![CDATA[webhook_sid]]></referencesField>
|
||||
<referencesTable><![CDATA[webhooks]]></referencesTable>
|
||||
<sourceCardinality>4</sourceCardinality>
|
||||
<destinationCardinality>2</destinationCardinality>
|
||||
<sourceCardinality>2</sourceCardinality>
|
||||
<destinationCardinality>1</destinationCardinality>
|
||||
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
|
||||
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
|
||||
<forcedUnique><![CDATA[0]]></forcedUnique>
|
||||
<objectComment><![CDATA[webhook to call when devices underr this account attempt to register]]></objectComment>
|
||||
<uid><![CDATA[A75FAB8E-C2A1-4A05-A09E-6FF454109B6F]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[queue_event_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>2</sourceCardinality>
|
||||
<destinationCardinality>1</destinationCardinality>
|
||||
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
|
||||
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
|
||||
<forcedUnique><![CDATA[0]]></forcedUnique>
|
||||
<objectComment><![CDATA[webhook to call when members enter or leave a queue created by this account.]]></objectComment>
|
||||
<uid><![CDATA[3F1CD447-0ABF-4B4C-80A8-BAB0F73B64C9]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[device_calling_application_sid]]></name>
|
||||
<type><![CDATA[CHAR(36)]]></type>
|
||||
@@ -574,6 +627,19 @@
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[C7130A90-DBB4-424D-A9A9-CB203C32350C]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[webhook_secret]]></name>
|
||||
<type><![CDATA[VARCHAR(36)]]></type>
|
||||
<notNull><![CDATA[0]]></notNull>
|
||||
<uid><![CDATA[CF25660D-AACA-4783-8A13-C7393D3B3D95]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[disable_cdrs]]></name>
|
||||
<type><![CDATA[BOOLEAN]]></type>
|
||||
<defaultValue><![CDATA[0]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[341B2A8D-AE85-4FCA-8EA8-D6E6149511F4]]></uid>
|
||||
</SQLField>
|
||||
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
|
||||
<objectComment><![CDATA[An enterprise that uses the platform for comm services]]></objectComment>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
@@ -585,8 +651,8 @@
|
||||
<comment><![CDATA[A phone number that has been assigned to an account]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>385.00</x>
|
||||
<y>625.00</y>
|
||||
<x>360.00</x>
|
||||
<y>753.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>331.00</width>
|
||||
@@ -757,14 +823,13 @@
|
||||
<name><![CDATA[applications]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A defined set of behaviors to be applied to phone calls ]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>829.00</x>
|
||||
<y>568.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>345.00</width>
|
||||
<height>240.00</height>
|
||||
<height>260.00</height>
|
||||
</size>
|
||||
<zorder>0</zorder>
|
||||
<SQLField>
|
||||
@@ -812,7 +877,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>
|
||||
@@ -829,6 +894,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>
|
||||
@@ -1005,7 +1084,6 @@
|
||||
<name><![CDATA[service_providers]]></name>
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A partition of the platform used by one service provider]]></comment>
|
||||
<tableType><![CDATA[InnoDB]]></tableType>
|
||||
<location>
|
||||
<x>838.00</x>
|
||||
<y>96.00</y>
|
||||
@@ -1083,17 +1161,17 @@
|
||||
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
|
||||
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
|
||||
<PageGridVisible><![CDATA[0]]></PageGridVisible>
|
||||
<RightSidebarWidth><![CDATA[1944.000000]]></RightSidebarWidth>
|
||||
<RightSidebarWidth><![CDATA[1415.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[1194.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[26.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[100.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 5}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[2221.000000]]></windowWidth>
|
||||
<windowHeight><![CDATA[1042.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[3550.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[1884.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{369.5, 4}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1692.000000]]></windowWidth>
|
||||
</SQLDocumentInfo>
|
||||
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
|
||||
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const getMysqlConnection = require('./mysql');
|
||||
const promisePool = require('./pool');
|
||||
|
||||
module.exports = {
|
||||
getMysqlConnection
|
||||
getMysqlConnection,
|
||||
promisePool
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ const mysql = require('mysql2');
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
|
||||
10
lib/db/pool.js
Normal file
10
lib/db/pool.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const mysql = require('mysql2');
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
});
|
||||
module.exports = pool.promise();
|
||||
@@ -3,17 +3,28 @@ const {getMysqlConnection} = require('../db');
|
||||
|
||||
const retrieveSql = `SELECT * from accounts acc
|
||||
LEFT JOIN webhooks AS rh
|
||||
ON acc.registration_hook_sid = rh.webhook_sid`;
|
||||
ON acc.registration_hook_sid = rh.webhook_sid
|
||||
LEFT JOIN webhooks AS qh
|
||||
ON acc.queue_event_hook_sid = qh.webhook_sid`;
|
||||
|
||||
|
||||
function transmogrifyResults(results) {
|
||||
return results.map((row) => {
|
||||
const obj = row.acc;
|
||||
let obj = row.acc;
|
||||
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
|
||||
Object.assign(obj, {registration_hook: row.rh});
|
||||
obj = {...obj, registration_hook: row.rh};
|
||||
delete obj.registration_hook.webhook_sid;
|
||||
}
|
||||
else obj.registration_hook = null;
|
||||
|
||||
if (row.qh && Object.keys(row.qh).length && row.qh.url !== null) {
|
||||
obj = {...obj, queue_event_hook: row.qh};
|
||||
delete obj.queue_event_hook.webhook_sid;
|
||||
}
|
||||
else obj.queue_event_hook = null;
|
||||
|
||||
delete obj.registration_hook_sid;
|
||||
delete obj.queue_event_hook_sid;
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
@@ -100,6 +111,10 @@ Account.fields = [
|
||||
name: 'registration_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'queue_event_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'device_calling_application_sid',
|
||||
type: 'string',
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -33,6 +33,22 @@ VoipCarrier.fields = [
|
||||
{
|
||||
name: 'e164_leading_plus',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'requires_register',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'register_username',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'register_sip_realm',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'register_password',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -5,16 +5,15 @@ const Account = require('../../models/account');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const ApiKey = require('../../models/api-key');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
const decorate = require('./decorate');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const snakeCase = require('../../utils/snake-case');
|
||||
const sysError = require('./error');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate,
|
||||
'delete': validateDelete
|
||||
};
|
||||
const {promisePool} = require('../../db');
|
||||
const {hasAccountPermissions} = require('./utils');
|
||||
let idx = 0;
|
||||
|
||||
router.use('/:sid/Queues', hasAccountPermissions, require('./queues'));
|
||||
|
||||
function coerceNumbers(callInfo) {
|
||||
if (Array.isArray(callInfo)) {
|
||||
return callInfo.map((ci) => {
|
||||
@@ -42,6 +41,7 @@ function validateUpdateCall(opts) {
|
||||
const hasWhisper = opts.whisper;
|
||||
const count = [
|
||||
'call_hook',
|
||||
'child_call_hook',
|
||||
'call_status',
|
||||
'listen_status',
|
||||
'mute_status']
|
||||
@@ -55,11 +55,13 @@ function validateUpdateCall(opts) {
|
||||
case 1:
|
||||
// good
|
||||
break;
|
||||
case 2:
|
||||
if (opts.call_hook && opts.child_call_hook) break;
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default:
|
||||
throw new DbErrorBadRequest('multiple options are not allowed in updateCall');
|
||||
}
|
||||
|
||||
if (opts.call_hook && !opts.call_hook.url) throw new DbErrorBadRequest('missing call_hook.url');
|
||||
if (opts.call_status && !['completed', 'no-answer'].includes(opts.call_status)) {
|
||||
throw new DbErrorBadRequest('invalid call_status');
|
||||
}
|
||||
@@ -75,6 +77,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':
|
||||
@@ -129,8 +132,58 @@ async function validateCreateCall(logger, sid, req) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!obj.call_hook || (obj.call_hook && !obj.call_hook.url)) {
|
||||
throw new DbErrorBadRequest('either url or application_sid required');
|
||||
if (!obj.call_hook && !obj.application_sid) {
|
||||
throw new DbErrorBadRequest('either call_hook or application_sid required');
|
||||
}
|
||||
if (typeof obj.call_hook === 'string') {
|
||||
const url = obj.call_hook;
|
||||
obj.call_hook = {
|
||||
url,
|
||||
method: 'POST'
|
||||
};
|
||||
}
|
||||
if (typeof obj.call_status_hook === 'string') {
|
||||
const url = obj.call_status_hook;
|
||||
obj.call_status_hook = {
|
||||
url,
|
||||
method: 'POST'
|
||||
};
|
||||
}
|
||||
if (typeof obj.call_hook === 'object' && typeof obj.call_hook.url != 'string') {
|
||||
throw new DbErrorBadRequest('call_hook must be string or an object containing a url property');
|
||||
}
|
||||
if (typeof obj.call_status_hook === 'object' && typeof obj.call_status_hook.url != 'string') {
|
||||
throw new DbErrorBadRequest('call_status_hook must be string or an object containing a url property');
|
||||
}
|
||||
if (obj.call_hook && !/^https?:/.test(obj.call_hook.url)) {
|
||||
throw new DbErrorBadRequest('call_hook url be an absolute url');
|
||||
}
|
||||
if (obj.call_status_hook && !/^https?:/.test(obj.call_status_hook.url)) {
|
||||
throw new DbErrorBadRequest('call_status_hook url be an absolute url');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +205,9 @@ async function validateAdd(req) {
|
||||
if (req.body.registration_hook && typeof req.body.registration_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'registration_hook\' must be an object when adding an account');
|
||||
}
|
||||
if (req.body.queue_event_hook && typeof req.body.queue_event_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'queue_event_hook\' must be an object when adding an account');
|
||||
}
|
||||
}
|
||||
async function validateUpdate(req, sid) {
|
||||
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
@@ -180,7 +236,32 @@ async function validateDelete(req, sid) {
|
||||
}
|
||||
}
|
||||
|
||||
decorate(router, Account, ['delete'], preconditions);
|
||||
/* delete */
|
||||
router.delete('/:sid', async(req, res) => {
|
||||
const sid = req.params.sid;
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
await validateDelete(req, sid);
|
||||
|
||||
/* delete associated webhooks */
|
||||
const webhooks = [];
|
||||
const [results] = await promisePool.query('SELECT * FROM accounts where account_sid = ?', sid);
|
||||
if (results.length) {
|
||||
if (results[0].registration_hook_sid) webhooks.push(results[0].registration_hook_sid);
|
||||
if (results[0].queue_event_hook_sid) webhooks.push(results[0].queue_event_hook_sid);
|
||||
}
|
||||
await Account.remove(sid);
|
||||
|
||||
for (const wh of webhooks) {
|
||||
promisePool.execute('DELETE from webhooks where webhook_sid = ?', [wh])
|
||||
.catch((err) => logger.info({err, webhooks}, 'DELETE /Accounts Error deleting webhooks'));
|
||||
}
|
||||
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* add */
|
||||
router.post('/', async(req, res) => {
|
||||
@@ -190,7 +271,7 @@ router.post('/', async(req, res) => {
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
for (const prop of ['registration_hook']) {
|
||||
for (const prop of ['registration_hook', 'queue_event_hook']) {
|
||||
if (obj[prop]) {
|
||||
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
|
||||
delete obj[prop];
|
||||
@@ -237,9 +318,10 @@ 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']) {
|
||||
for (const prop of ['registration_hook', 'queue_event_hook']) {
|
||||
if (prop in obj && Object.keys(obj[prop]).length) {
|
||||
if ('webhook_sid' in obj[prop]) {
|
||||
const sid = obj[prop]['webhook_sid'];
|
||||
@@ -251,17 +333,39 @@ router.put('/:sid', async(req, res) => {
|
||||
obj[`${prop}_sid`] = sid;
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj[`${prop}_sid`] = null;
|
||||
}
|
||||
delete obj[prop];
|
||||
}
|
||||
|
||||
await validateUpdate(req, sid);
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
|
||||
if (Object.keys(obj).length) {
|
||||
const orphanedHooks = [];
|
||||
if (null === obj.registration_hook || null == obj.queue_event_hook) {
|
||||
const results = await Account.retrieve(sid);
|
||||
if (results.length) {
|
||||
if (results[0].registration_hook_sid) orphanedHooks.push(results[0].registration_hook_sid);
|
||||
if (results[0].queue_event_hook_sid) orphanedHooks.push(results[0].queue_event_hook_sid);
|
||||
}
|
||||
if (null === obj.registration_hook) {
|
||||
obj.registration_hook_sid = null;
|
||||
delete obj.registration_hook;
|
||||
}
|
||||
if (null === obj.queue_event_hook) {
|
||||
obj.queue_event_hook_sid = null;
|
||||
delete obj.queue_event_hook;
|
||||
}
|
||||
}
|
||||
logger.info({obj}, `about to update Account ${sid}`);
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
}
|
||||
if (orphanedHooks.length) {
|
||||
for (const sid in orphanedHooks) {
|
||||
await Webhook.remove(sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
@@ -306,7 +410,7 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
url: serviceUrl,
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: req.body
|
||||
body: Object.assign(req.body, {account_sid: sid})
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error sending createCall POST to ${ip}`);
|
||||
@@ -391,7 +495,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
|
||||
/**
|
||||
* update a call
|
||||
*/
|
||||
router.post('/:sid/Calls/:callSid', async(req, res) => {
|
||||
const updateCall = async(req, res) => {
|
||||
const accountSid = req.params.sid;
|
||||
const callSid = req.params.callSid;
|
||||
const {logger, retrieveCall} = req.app.locals;
|
||||
@@ -417,7 +521,57 @@ router.post('/:sid/Calls/:callSid', async(req, res) => {
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
};
|
||||
|
||||
/** leaving for legacy purposes, this should have been (and now is) a PUT */
|
||||
router.post('/:sid/Calls/:callSid', async(req, res) => {
|
||||
await updateCall(req, res);
|
||||
});
|
||||
router.put('/:sid/Calls/:callSid', async(req, res) => {
|
||||
await updateCall(req, res);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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(), account_sid: sid}, 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;
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -20,4 +20,8 @@ api.use('/Sbcs', isAdminScope, require('./sbcs'));
|
||||
api.use('/Users', require('./users'));
|
||||
api.use('/login', require('./login'));
|
||||
|
||||
// messaging
|
||||
api.use('/messaging', require('./sms-inbound')); // inbound SMS from carrier
|
||||
api.use('/outboundSMS', require('./sms-outbound')); // outbound SMS from feature server
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -9,10 +9,16 @@ const preconditions = {
|
||||
'delete': checkInUse,
|
||||
'update': validateUpdate
|
||||
};
|
||||
const sysError = require('./error');
|
||||
|
||||
/* check for required fields when adding */
|
||||
async function validateAdd(req) {
|
||||
try {
|
||||
/* account level user can only act on carriers associated to his/her account */
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
}
|
||||
|
||||
if (!req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid is required');
|
||||
if (!req.body.number) throw new DbErrorBadRequest('number is required');
|
||||
validateNumber(req.body.number);
|
||||
@@ -30,24 +36,55 @@ async function validateAdd(req) {
|
||||
/* can not delete a phone number if it in use */
|
||||
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');
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
|
||||
}
|
||||
}
|
||||
if (!req.user.hasAccountAuth && phoneNumber.account_sid) {
|
||||
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);
|
||||
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');
|
||||
|
||||
// TODO: if we are assigning to an account, verify it exists
|
||||
|
||||
// TODO: if we are assigning to an application, verify it is associated to the same account
|
||||
|
||||
// TODO: if we are removing from an account, verify we are also removing from application.
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decorate(router, PhoneNumber, ['*'], preconditions);
|
||||
decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/* retrieve */
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await PhoneNumber.retrieve(req.params.sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
20
lib/routes/api/queues.js
Normal file
20
lib/routes/api/queues.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('./error');
|
||||
const {parseAccountSid} = require('./utils');
|
||||
|
||||
/**
|
||||
* retrieve queues for an account
|
||||
*/
|
||||
router.get('/', async(req, res) => {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const {logger, listQueues} = req.app.locals;
|
||||
try {
|
||||
const queues = await listQueues(account_sid, req.query.name);
|
||||
res.status(200).json(queues);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -97,6 +97,4 @@ router.put('/:sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
142
lib/routes/api/sms-inbound.js
Normal file
142
lib/routes/api/sms-inbound.js
Normal 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;
|
||||
44
lib/routes/api/sms-outbound.js
Normal file
44
lib/routes/api/sms-outbound.js
Normal 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;
|
||||
40
lib/routes/api/utils.js
Normal file
40
lib/routes/api/utils.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const parseServiceProviderSid = (req) => {
|
||||
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
const parseAccountSid = (req) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
|
||||
if (arr) return arr[1];
|
||||
};
|
||||
|
||||
const hasAccountPermissions = (req, res, next) => {
|
||||
if (req.user.hasScope('admin')) return next();
|
||||
if (req.user.hasScope('account')) {
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (account_sid === req.user.account_sid) return next();
|
||||
}
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
const hasServiceProviderPermissions = (req, res, next) => {
|
||||
if (req.user.hasScope('admin')) return next();
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
if (service_provider_sid === req.user.service_provider_sid) return next();
|
||||
}
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
parseAccountSid,
|
||||
parseServiceProviderSid,
|
||||
hasAccountPermissions,
|
||||
hasServiceProviderPermissions
|
||||
};
|
||||
@@ -177,7 +177,8 @@ paths:
|
||||
description: login succeeded
|
||||
content:
|
||||
application/json:
|
||||
schema: '#/components/schemas/Login'
|
||||
schema:
|
||||
$ref: '#/components/schemas/Login'
|
||||
403:
|
||||
description: login failed
|
||||
content:
|
||||
@@ -249,10 +250,29 @@ paths:
|
||||
name:
|
||||
type: string
|
||||
description: voip carrier name
|
||||
example: fastco
|
||||
description:
|
||||
type: string
|
||||
example: my US sip trunking provider
|
||||
e164_leading_plus:
|
||||
type: boolean
|
||||
description: whether a leading + is required on INVITEs to this provider
|
||||
example: true
|
||||
requires_register:
|
||||
type: boolean
|
||||
description: wehther this provider requires us to send a REGISTER to them in order to receive calls
|
||||
register_username:
|
||||
type: string
|
||||
description: sip username to authenticate with, if registration is required
|
||||
example: foo
|
||||
register_sip_realm:
|
||||
type: string
|
||||
description: sip realm to authenticate with, if registration is required
|
||||
example: sip.fastco.com
|
||||
register_password:
|
||||
type: string
|
||||
description: sip password to authenticate with, if registration is required
|
||||
example: bar
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
@@ -961,12 +981,9 @@ paths:
|
||||
registration_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: authentication webhook for registration
|
||||
device_calling_hook:
|
||||
queue_event_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: webhook for inbound call from registered devices
|
||||
error_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: webhook for reporting errors from malformed applications
|
||||
description: webhook for queue events
|
||||
service_provider_sid:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -1249,10 +1266,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:
|
||||
@@ -1382,7 +1402,36 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
|
||||
|
||||
/Accounts/{AccountSid}/Queues:
|
||||
get:
|
||||
summary: list queues for an Account
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: name
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: a queue name or glob-style pattern matching one or more queues to return
|
||||
responses:
|
||||
200:
|
||||
description: list of queues for this account matching the name query param, if provided
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Queue'
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
|
||||
/Accounts/{AccountSid}/Calls:
|
||||
post:
|
||||
summary: create a call
|
||||
@@ -1433,9 +1482,9 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- callSid
|
||||
- sid
|
||||
properties:
|
||||
callSid:
|
||||
sid:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 2531329f-fb09-4ef7-887e-84e648214436
|
||||
@@ -1465,8 +1514,6 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
|
||||
|
||||
/Accounts/{AccountSid}/Calls/{CallSid}:
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
@@ -1567,7 +1614,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:
|
||||
@@ -1649,6 +1728,14 @@ components:
|
||||
type: string
|
||||
e164_leading_plus:
|
||||
type: boolean
|
||||
requires_register:
|
||||
type: boolean
|
||||
register_username:
|
||||
type: string
|
||||
register_sip_realm:
|
||||
type: string
|
||||
register_password:
|
||||
type: string
|
||||
required:
|
||||
- voip_carrier_sid
|
||||
- name
|
||||
@@ -1689,19 +1776,19 @@ components:
|
||||
registration_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: authentication webhook for registration
|
||||
device_calling_hook:
|
||||
queue_event_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: webhook for inbound call from registered devices
|
||||
error_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: webhook for reporting errors from malformed applications
|
||||
description: webhook for queue events
|
||||
device_calling_application_sid:
|
||||
type: string
|
||||
format: uuid
|
||||
service_provider_sid:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- account_sid
|
||||
- name
|
||||
- service_provider
|
||||
- service_provider_sid
|
||||
Application:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1715,10 +1802,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:
|
||||
@@ -1731,8 +1821,6 @@ components:
|
||||
- application_sid
|
||||
- name
|
||||
- account_sid
|
||||
- inbound_hook
|
||||
- inbound_status_hook
|
||||
ApiKey:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1914,6 +2002,34 @@ 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"}
|
||||
Queue:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: support
|
||||
length:
|
||||
type: number
|
||||
format: integer
|
||||
example: 5
|
||||
required:
|
||||
- name
|
||||
- length
|
||||
example: {"name": "new-orders", length: 12}
|
||||
security:
|
||||
- bearerAuth: []
|
||||
40
lib/utils/sms-provider.js
Normal file
40
lib/utils/sms-provider.js
Normal 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;
|
||||
|
||||
19
package.json
19
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "1.1.7",
|
||||
"version": "1.2.1",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=localhost JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ | ./node_modules/.bin/tap-spec",
|
||||
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_MYSQL_PORT=3360 JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ",
|
||||
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
@@ -15,27 +16,25 @@
|
||||
"url": "https://github.com/jambonz/jambonz-api-server.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.3.8",
|
||||
"@jambonz/realtimedb-helpers": "0.2.15",
|
||||
"@jambonz/db-helpers": "^0.6.11",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"mysql2": "^2.1.0",
|
||||
"mysql2": "^2.2.5",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pino": "^5.17.0",
|
||||
"request": "^2.88.2",
|
||||
"request-debug": "^0.2.0",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"swagger-ui-express": "^4.1.5",
|
||||
"uuid": "^3.4.0",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "^7.15.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"tap-dot": "^2.0.0",
|
||||
"tap-spec": "^5.0.0",
|
||||
"tape": "^4.13.3"
|
||||
"tape": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -16,6 +16,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
|
||||
test('account tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const {pushBack} = app.locals;
|
||||
let sid;
|
||||
try {
|
||||
let result;
|
||||
@@ -36,6 +37,10 @@ test('account tests', async(t) => {
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
queue_event_hook: {
|
||||
url: 'http://example.com/q',
|
||||
method: 'post'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -68,9 +73,10 @@ test('account tests', async(t) => {
|
||||
json: true,
|
||||
});
|
||||
let regHook = result[0].registration_hook;
|
||||
let qHook = result[0].queue_event_hook;
|
||||
t.ok(result.length === 1 &&
|
||||
Object.keys(regHook).length == 4, 'successfully queried all accounts');
|
||||
|
||||
Object.keys(regHook).length == 4 && Object.keys(qHook).length == 4, 'successfully queried all accounts');
|
||||
|
||||
/* query one accounts */
|
||||
result = await request.get(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
@@ -104,7 +110,6 @@ test('account tests', async(t) => {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
//console.log(`retrieved account after update: ${JSON.stringify(result)}`);
|
||||
t.ok(Object.keys(result.registration_hook).length === 4, 'successfully removed a hook from account');
|
||||
|
||||
/* assign phone number to account */
|
||||
@@ -118,6 +123,36 @@ test('account tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully assigned phone number to account');
|
||||
|
||||
/* retrieve queues for account */
|
||||
result = await request.get(`/Accounts/${sid}/Queues`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200 && 0 === result.body.length,
|
||||
'successfully retrieved an empty array when no queues exist');
|
||||
|
||||
await pushBack(`queue:${sid}:customer-support`, 'https://ip:300/v1/enqueue/foobar');
|
||||
await pushBack(`queue:${sid}:customer-support`, 'https://ip:300/v1/enqueue/bazzle');
|
||||
await pushBack(`queue:${sid}:sales-new-orders`, 'https://ip:300/v1/enqueue/bazzle');
|
||||
await pushBack(`queue:${sid}:sales-returns`, 'https://ip:300/v1/enqueue/bazzle');
|
||||
|
||||
result = await request.get(`/Accounts/${sid}/Queues`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
console.log(`retrieved queues: ${result.statusCode}: ${JSON.stringify(result.body)}`);
|
||||
//t.ok(result.statusCode === 200 && 0 === result.body.length,
|
||||
// 'successfully retrieved an empty array when no queues exist');
|
||||
|
||||
result = await request.get(`/Accounts/${sid}/Queues?name=sales-*`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
console.log(`retrieved queues: ${result.statusCode}: ${JSON.stringify(result.body)}`);
|
||||
|
||||
/* cannot delete account that has phone numbers assigned */
|
||||
result = await request.delete(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
@@ -138,7 +173,7 @@ test('account tests', async(t) => {
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -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,
|
||||
@@ -107,7 +121,7 @@ test('application tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
//console.error(err);
|
||||
|
||||
@@ -461,7 +461,7 @@ test('authentication tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/ServiceProviders', spA_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', spB_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
|
||||
|
||||
test('creating jambones_test database', (t) => {
|
||||
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/../db/create_test_db.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/../db/create_test_db.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('database successfully created');
|
||||
t.end();
|
||||
@@ -11,7 +10,7 @@ test('creating jambones_test database', (t) => {
|
||||
});
|
||||
|
||||
test('creating schema', (t) => {
|
||||
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('schema successfully created');
|
||||
t.end();
|
||||
@@ -19,7 +18,7 @@ test('creating schema', (t) => {
|
||||
});
|
||||
|
||||
test('creating auth token', (t) => {
|
||||
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('auth token successfully created');
|
||||
t.end();
|
||||
|
||||
25
test/docker-compose-testbed.yaml
Normal file
25
test/docker-compose-testbed.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
ports:
|
||||
- "3360:3306"
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
redis:
|
||||
image: redis:5-alpine
|
||||
ports:
|
||||
- "16379:6379/tcp"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8-alpine
|
||||
ports:
|
||||
- "8086:8086"
|
||||
11
test/docker_start.js
Normal file
11
test/docker_start.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const test = require('tape');
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('starting docker network..', (t) => {
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
|
||||
setTimeout(() => {
|
||||
t.pass('docker started');
|
||||
t.end(err);
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
12
test/docker_stop.js
Normal file
12
test/docker_stop.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const test = require('tape');
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('stopping docker network..', (t) => {
|
||||
t.timeoutAfter(10000);
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err, stdout, stderr) => {
|
||||
//console.log(`stderr: ${stderr}`);
|
||||
process.exit(0);
|
||||
});
|
||||
t.end() ;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./docker_start');
|
||||
require('./create-test-db');
|
||||
require('./sip-gateways');
|
||||
require('./service-providers');
|
||||
@@ -8,4 +9,4 @@ require('./applications');
|
||||
require('./auth');
|
||||
require('./sbcs');
|
||||
require('./ms-teams');
|
||||
require('./remove-test-db');
|
||||
require('./docker_stop');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -10,7 +10,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
test('sbc_addresses tests', async(t) => {
|
||||
test('ms teams tests', async(t) => {
|
||||
const app = require('../app');
|
||||
let sid;
|
||||
try {
|
||||
@@ -79,7 +79,7 @@ test('sbc_addresses tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid2);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -111,7 +111,7 @@ test('phone number tests', async(t) => {
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
|
||||
const pwd = process.env.CI ? '' : '-p$MYSQL_ROOT_PASSWORD';
|
||||
|
||||
test('dropping jambones_test database', (t) => {
|
||||
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/../db/remove_test_db.sql`, (err, stdout, stderr) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root ${pwd} --protocol=tcp < ${__dirname}/../db/remove_test_db.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('database successfully dropped');
|
||||
t.end();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -60,7 +60,7 @@ test('sbc_addresses tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/Sbcs', sid2);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
83
test/serve-integration.js
Normal file
83
test/serve-integration.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const exec = require('child_process').exec ;
|
||||
let stopping = false;
|
||||
|
||||
process.on('SIGINT', async() => {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
console.log('shutting down');
|
||||
await stopDocker();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
const startDocker = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('starting dockerized mysql and redis..')
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err) => {
|
||||
if (err) return reject(err);
|
||||
setTimeout(() => {
|
||||
console.log('mysql is running');
|
||||
resolve();
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createDb = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('creating database..')
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/../db/create_test_db.sql`, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createSchema = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('creating schema..')
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const seedDb = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('seeding database..')
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-default-service-provider-and-account.sql`, (err) => {
|
||||
if (err) return reject(err);
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err) => {
|
||||
if (err) return reject(err);
|
||||
exec(`node ${__dirname}/../db/reset_admin_password.js`, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const stopDocker = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('stopping docker network..')
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
startDocker()
|
||||
.then(createDb)
|
||||
.then(createSchema)
|
||||
.then(seedDb)
|
||||
.then(() => {
|
||||
console.log('ready for testing!');
|
||||
require('..');
|
||||
})
|
||||
.catch(async(err) => {
|
||||
console.error({err}, 'Error running integration test');
|
||||
await stopDocker();
|
||||
process.exit(-1);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -117,7 +117,7 @@ test('service provider tests', async(t) => {
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted service provider 2');
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -72,7 +72,7 @@ test('sip gateway tests', async(t) => {
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
@@ -50,7 +50,11 @@ test('voip carrier tests', async(t) => {
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
body: {
|
||||
name: 'robb'
|
||||
name: 'robb',
|
||||
requires_register: true,
|
||||
register_username: 'foo',
|
||||
register_sip_realm: 'bar',
|
||||
register_password: 'baz'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated voip carrier');
|
||||
@@ -190,7 +194,7 @@ test('voip carrier tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid2);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
t.end();
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user