initial checkin

This commit is contained in:
Dave Horton
2019-11-27 09:23:14 -05:00
commit 8c287f06df
18 changed files with 2703 additions and 0 deletions

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
test/*

126
.eslintrc.json Normal file
View File

@@ -0,0 +1,126 @@
{
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": false,
"modules": false
},
"ecmaVersion": 2017
},
"plugins": ["promise"],
"rules": {
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/no-return-in-finally": "warn",
// Possible Errors
// http://eslint.org/docs/rules/#possible-errors
"comma-dangle": [2, "only-multiline"],
"no-control-regex": 2,
"no-debugger": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast" : 2,
"no-extra-parens": [2, "functions"],
"no-extra-semi": 2,
"no-func-assign": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-proto": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
"use-isnan": 2,
"valid-typeof": 2,
// Best Practices
// http://eslint.org/docs/rules/#best-practices
"no-fallthrough": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-self-assign": 2,
"no-unused-labels": 2,
// Strict Mode
// http://eslint.org/docs/rules/#strict-mode
"strict": [2, "never"],
// Variables
// http://eslint.org/docs/rules/#variables
"no-delete-var": 2,
"no-undef": 2,
"no-unused-vars": [2, {"args": "none"}],
// Node.js and CommonJS
// http://eslint.org/docs/rules/#nodejs-and-commonjs
"no-mixed-requires": 2,
"no-new-require": 2,
"no-path-concat": 2,
"no-restricted-modules": [2, "sys", "_linklist"],
// Stylistic Issues
// http://eslint.org/docs/rules/#stylistic-issues
"comma-spacing": 2,
"eol-last": 2,
"indent": [2, 2, {"SwitchCase": 1}],
"keyword-spacing": 2,
"max-len": [2, 120, 2],
"new-parens": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2, {"max": 2}],
"no-trailing-spaces": [2, {"skipBlankLines": false }],
"quotes": [2, "single", "avoid-escape"],
"semi": 2,
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": 2,
// ECMAScript 6
// http://eslint.org/docs/rules/#ecmascript-6
"arrow-parens": [2, "always"],
"arrow-spacing": [2, {"before": true, "after": true}],
"constructor-super": 2,
"no-class-assign": 2,
"no-confusing-arrow": 2,
"no-const-assign": 2,
"no-dupe-class-members": 2,
"no-new-symbol": 2,
"no-this-before-super": 2,
"prefer-const": 2
},
"globals": {
"DTRACE_HTTP_CLIENT_REQUEST" : false,
"LTTNG_HTTP_CLIENT_REQUEST" : false,
"COUNTER_HTTP_CLIENT_REQUEST" : false,
"DTRACE_HTTP_CLIENT_RESPONSE" : false,
"LTTNG_HTTP_CLIENT_RESPONSE" : false,
"COUNTER_HTTP_CLIENT_RESPONSE" : false,
"DTRACE_HTTP_SERVER_REQUEST" : false,
"LTTNG_HTTP_SERVER_REQUEST" : false,
"COUNTER_HTTP_SERVER_REQUEST" : false,
"DTRACE_HTTP_SERVER_RESPONSE" : false,
"LTTNG_HTTP_SERVER_RESPONSE" : false,
"COUNTER_HTTP_SERVER_RESPONSE" : false,
"DTRACE_NET_STREAM_END" : false,
"LTTNG_NET_STREAM_END" : false,
"COUNTER_NET_SERVER_CONNECTION_CLOSE" : false,
"DTRACE_NET_SERVER_CONNECTION" : false,
"LTTNG_NET_SERVER_CONNECTION" : false,
"COUNTER_NET_SERVER_CONNECTION" : false
}
}

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# Logs
logs
*.log
package-lock.json
# Runtime data
pids
*.pid
*.seed
# github pages site
_site
#transient test cases
examples/nosave.*.js
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul and nyc
coverage
.nyc_output/
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
.DS_Store
examples/*
create_db.sql

1
README.md Normal file
View File

@@ -0,0 +1 @@
# jambones-api-server

23
app.js Normal file
View File

@@ -0,0 +1,23 @@
const config = require('config');
const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, config.get('logging'));
const logger = require('pino')(opts);
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const passport = require('passport');
const authStrategy = require('./lib/auth')(logger);
const routes = require('./lib/routes');
const PORT = process.env.HTTP_PORT || 3000;
passport.use(authStrategy);
app.locals.logger = logger;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use('/v1', passport.authenticate('bearer', { session: false }));
app.use('/', routes);
app.listen(PORT);

View File

@@ -0,0 +1,12 @@
{
"logging": {
"level": "info"
},
"mysql": {
"host": "example.org",
"user": "bob",
"password": "secret",
"database": "jambones",
"connectionLimit": 10
}
}

284
db/jambones-sql.sql Normal file
View File

@@ -0,0 +1,284 @@
/* SQLEditor (MySQL (2))*/
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS `call_routes`;
DROP TABLE IF EXISTS `conference_participants`;
DROP TABLE IF EXISTS `queue_members`;
DROP TABLE IF EXISTS `calls`;
DROP TABLE IF EXISTS `phone_numbers`;
DROP TABLE IF EXISTS `applications`;
DROP TABLE IF EXISTS `conferences`;
DROP TABLE IF EXISTS `old_call`;
DROP TABLE IF EXISTS `queues`;
DROP TABLE IF EXISTS `subscriptions`;
DROP TABLE IF EXISTS `registered_users`;
DROP TABLE IF EXISTS `accounts`;
DROP TABLE IF EXISTS `service_providers`;
DROP TABLE IF EXISTS `phone_number_inventory`;
DROP TABLE IF EXISTS `voip_carriers`;
CREATE TABLE IF NOT EXISTS `api_keys`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`token` CHAR(36) NOT NULL UNIQUE ,
`account_id` INTEGER(10) UNSIGNED,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
CREATE TABLE IF NOT EXISTS `applications`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL,
`account_id` INTEGER(10) UNSIGNED NOT NULL,
`call_hook` VARCHAR(255),
`call_status_hook` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls with';
CREATE TABLE IF NOT EXISTS `call_routes`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`account_id` INTEGER(10) UNSIGNED NOT NULL,
`regex` VARCHAR(255) NOT NULL,
`application_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `conferences`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An audio conference';
CREATE TABLE IF NOT EXISTS `conference_participants`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`call_id` INTEGER(10) UNSIGNED,
`conference_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a conference that it is co';
CREATE TABLE IF NOT EXISTS `old_call`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`parent_call_id` INTEGER(10) UNSIGNED UNIQUE ,
`application_id` INTEGER(10) UNSIGNED,
`status_url` VARCHAR(255),
`time_start` DATETIME NOT NULL,
`time_alerting` DATETIME,
`time_answered` DATETIME,
`time_ended` DATETIME,
`direction` ENUM('inbound','outbound'),
`inbound_phone_number_id` INTEGER(10) UNSIGNED,
`inbound_user_id` INTEGER(10) UNSIGNED,
`outbound_user_id` INTEGER(10) UNSIGNED,
`calling_number` VARCHAR(255),
`called_number` VARCHAR(255),
`caller_name` VARCHAR(255),
`status` VARCHAR(255) NOT NULL COMMENT 'Possible values are queued, ringing, in-progress, completed, failed, busy and no-answer',
`sip_uri` VARCHAR(255) NOT NULL,
`sip_call_id` VARCHAR(255) NOT NULL,
`sip_cseq` INTEGER NOT NULL,
`sip_from_tag` VARCHAR(255) NOT NULL,
`sip_via_branch` VARCHAR(255) NOT NULL,
`sip_contact` VARCHAR(255),
`sip_final_status` INTEGER UNSIGNED,
`sdp_offer` VARCHAR(4096),
`sdp_answer` VARCHAR(4096),
`source_address` VARCHAR(255) NOT NULL,
`source_port` INTEGER UNSIGNED NOT NULL,
`dest_address` VARCHAR(255),
`dest_port` INTEGER UNSIGNED,
`url` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `phone_numbers`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`account_id` INTEGER(10) UNSIGNED NOT NULL,
`application_id` INTEGER(10) UNSIGNED,
`phone_number_inventory_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE TABLE IF NOT EXISTS `queues`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A set of behaviors to be applied to parked calls';
CREATE TABLE IF NOT EXISTS `queue_members`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`call_id` INTEGER(10) UNSIGNED,
`queue_id` INTEGER(10) UNSIGNED NOT NULL,
`position` INTEGER,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a queue that it is waiting';
CREATE TABLE IF NOT EXISTS `registered_users`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`username` VARCHAR(255) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`sip_contact` VARCHAR(255) NOT NULL,
`sip_user_agent` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An active sip registration';
CREATE TABLE IF NOT EXISTS `calls`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`parent_call_id` INTEGER(10) UNSIGNED UNIQUE ,
`application_id` INTEGER(10) UNSIGNED,
`status_url` VARCHAR(255),
`time_start` DATETIME NOT NULL,
`time_alerting` DATETIME,
`time_answered` DATETIME,
`time_ended` DATETIME,
`direction` ENUM('inbound','outbound'),
`inbound_phone_number_id` INTEGER(10) UNSIGNED,
`inbound_user_id` INTEGER(10) UNSIGNED,
`outbound_user_id` INTEGER(10) UNSIGNED,
`calling_number` VARCHAR(255),
`called_number` VARCHAR(255),
`caller_name` VARCHAR(255),
`status` VARCHAR(255) NOT NULL COMMENT 'Possible values are queued, ringing, in-progress, completed, failed, busy and no-answer',
`sip_uri` VARCHAR(255) NOT NULL,
`sip_call_id` VARCHAR(255) NOT NULL,
`sip_cseq` INTEGER NOT NULL,
`sip_from_tag` VARCHAR(255) NOT NULL,
`sip_via_branch` VARCHAR(255) NOT NULL,
`sip_contact` VARCHAR(255),
`sip_final_status` INTEGER UNSIGNED,
`sdp_offer` VARCHAR(4096),
`sdp_answer` VARCHAR(4096),
`source_address` VARCHAR(255) NOT NULL,
`source_port` INTEGER UNSIGNED NOT NULL,
`dest_address` VARCHAR(255),
`dest_port` INTEGER UNSIGNED,
`url` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A phone call';
CREATE TABLE IF NOT EXISTS `service_providers`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
`description` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An organization that provides communication services to its ';
CREATE TABLE IF NOT EXISTS `accounts`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL,
`sip_realm` VARCHAR(255),
`service_provider_id` INTEGER(10) UNSIGNED NOT NULL,
`registration_hook` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A single end-user of the platform';
CREATE TABLE IF NOT EXISTS `subscriptions`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`subscribed_user_id` INTEGER(10) UNSIGNED NOT NULL,
`event` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An active sip subscription';
CREATE TABLE IF NOT EXISTS `voip_carriers`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An external organization that can provide sip trunking and D';
CREATE TABLE IF NOT EXISTS `phone_number_inventory`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`voip_carrier_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='Telephone numbers (DIDs) that have been procured from a voip';
ALTER TABLE `api_keys` ADD FOREIGN KEY account_id_idxfk (`account_id`) REFERENCES `accounts` (`id`);
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
ALTER TABLE `applications` ADD FOREIGN KEY account_id_idxfk_1 (`account_id`) REFERENCES `accounts` (`id`);
ALTER TABLE `call_routes` ADD FOREIGN KEY account_id_idxfk_2 (`account_id`) REFERENCES `accounts` (`id`);
ALTER TABLE `call_routes` ADD FOREIGN KEY application_id_idxfk (`application_id`) REFERENCES `applications` (`id`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY call_id_idxfk (`call_id`) REFERENCES `calls` (`id`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_id_idxfk (`conference_id`) REFERENCES `conferences` (`id`);
ALTER TABLE `old_call` ADD FOREIGN KEY parent_call_id_idxfk (`parent_call_id`) REFERENCES `old_call` (`id`);
ALTER TABLE `old_call` ADD FOREIGN KEY application_id_idxfk_1 (`application_id`) REFERENCES `old_call` (`parent_call_id`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_id_idxfk_3 (`account_id`) REFERENCES `accounts` (`id`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_id_idxfk_2 (`application_id`) REFERENCES `applications` (`id`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY phone_number_inventory_id_idxfk (`phone_number_inventory_id`) REFERENCES `phone_number_inventory` (`id`);
ALTER TABLE `queue_members` ADD FOREIGN KEY call_id_idxfk_1 (`call_id`) REFERENCES `calls` (`id`);
ALTER TABLE `queue_members` ADD FOREIGN KEY queue_id_idxfk (`queue_id`) REFERENCES `queues` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_id_idxfk_1 (`parent_call_id`) REFERENCES `calls` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY application_id_idxfk_3 (`application_id`) REFERENCES `calls` (`parent_call_id`);
ALTER TABLE `calls` ADD FOREIGN KEY inbound_phone_number_id_idxfk (`inbound_phone_number_id`) REFERENCES `phone_numbers` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY inbound_user_id_idxfk (`inbound_user_id`) REFERENCES `registered_users` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY outbound_user_id_idxfk (`outbound_user_id`) REFERENCES `registered_users` (`id`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_id_idxfk (`service_provider_id`) REFERENCES `service_providers` (`id`);
ALTER TABLE `subscriptions` ADD FOREIGN KEY subscribed_user_id_idxfk (`subscribed_user_id`) REFERENCES `registered_users` (`id`);
ALTER TABLE `phone_number_inventory` ADD FOREIGN KEY voip_carrier_id_idxfk (`voip_carrier_id`) REFERENCES `voip_carriers` (`id`);

1138
db/jambones.sqs Normal file

File diff suppressed because one or more lines are too long

39
lib/auth/index.js Normal file
View File

@@ -0,0 +1,39 @@
const Strategy = require('passport-http-bearer').Strategy;
const {getMysqlConnection} = require('../db');
const sql = `
SELECT api_keys.uuid, accounts.uuid
FROM api_keys
LEFT JOIN accounts
ON api_keys.account_id = accounts.id`;
function makeStrategy(logger) {
return new Strategy(
function(token, done) {
logger.info(`validating with token ${token}`);
getMysqlConnection((err, conn) => {
if (err) {
logger.error(err, 'Error retrieving mysql connection');
return done(err);
}
conn.query({sql, nestTables: '_'}, [token], (err, results, fields) => {
conn.release();
if (err) {
logger.error(err, 'Error querying for api key');
return done(err);
}
if (0 == results.length) return done(null, false);
if (results.length > 1) {
logger.info(`api key ${token} exists in multiple rows of api_keys table!!`);
return done(null, false);
}
// found api key
return done(null,
{accountSid: results[0].accounts_uuid},
{scope: results[0].accounts_uuid ? ['user'] : ['admin']});
});
});
});
}
module.exports = makeStrategy;

5
lib/db/index.js Normal file
View File

@@ -0,0 +1,5 @@
const getMysqlConnection = require('./mysql');
module.exports = {
getMysqlConnection
};

13
lib/db/mysql.js Normal file
View File

@@ -0,0 +1,13 @@
const mysql = require('mysql');
const config = require('config');
const pool = mysql.createPool(config.get('mysql'));
pool.getConnection((err, conn) => {
if (err) return console.error(err, 'Error testing pool');
conn.ping((err) => {
if (err) return console.error(err, `Error pinging mysql at ${JSON.stringify(config.get('mysql'))}`);
console.log('successfully pinged mysql database');
});
});
module.exports = pool.getConnection.bind(pool);

View File

@@ -0,0 +1,23 @@
const Emitter = require('events');
const {getMysqlConnection} = require('../db');
const scrubIds = require('../utils/scrub-ids');
class ServiceProvider extends Emitter {
constructor() {
super();
}
static retrieveAll() {
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query('SELECT * from service_providers', (err, results, fields) => {
if (err) return reject(err);
resolve(scrubIds(results));
});
});
});
}
}
module.exports = ServiceProvider;

21
lib/routes/api/index.js Normal file
View File

@@ -0,0 +1,21 @@
const api = require('express').Router();
function isAdmin(req, res, next) {
if (req.authInfo.scope.includes('admin')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
}
function isUser(req, res, next) {
if (req.authInfo.scope.includes('user')) return next();
res.status(403).json({
status: 'fail',
message: 'end-user data can not be modified with admin privileges'
});
}
api.use('/ServiceProviders', isAdmin, require('./service-providers'));
module.exports = api;

View File

@@ -0,0 +1,28 @@
const router = require('express').Router();
const ServiceProvider = require('../../models/service-provider');
function sysError(logger, res, err) {
logger.error(err, 'Database error');
res.status(500).end();
}
/* return list of all service providers */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
logger.info(`user: ${JSON.stringify(req.user)}`);
logger.info(`scope: ${JSON.stringify(req.authInfo.scope)}`);
try {
const results = await ServiceProvider.retrieveAll();
res.status(200).json(results);
} catch (err) {
logger.error(err, 'Error retrieving service providers');
sysError(logger, res, err);
}
});
/* add a service provider */
router.post('/', (req, res) => {
});
module.exports = router;

23
lib/routes/index.js Normal file
View File

@@ -0,0 +1,23 @@
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const path = require('path');
const swaggerDocument = YAML.load(path.resolve(__dirname, '../swagger/swagger.yaml'));
const api = require('./api');
const routes = express.Router();
routes.use('/v1', api);
routes.use('/swagger', swaggerUi.serve);
routes.get('/swagger', swaggerUi.setup(swaggerDocument));
// health checks
routes.get('/', (req, res) => {
res.sendStatus(200);
});
routes.get('/health', (req, res) => {
res.sendStatus(200);
});
module.exports = routes;

895
lib/swagger/swagger.yaml Normal file
View File

@@ -0,0 +1,895 @@
openapi: 3.0.0
info:
title: Jambones REST API
description: Jambones REST API
contact:
email: daveh@drachtio.org
license:
name: MIT
url: https://opensource.org/licenses/MIT
version: 1.0.0
servers:
- url: /v1
description: development server
paths:
/ServiceProviders:
post:
summary: create service provider
operationId: createServiceProvider
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: service provider name
description:
type: string
required:
- name
responses:
201:
description: service provider successfully created
content:
application/json:
schema:
required:
- serviceProviderSid
properties:
serviceProviderSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
409:
description: an existing service provider already exists with this name
get:
summary: list service providers
operationId: listServiceProviders
responses:
200:
description: list of service providers
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ServiceProvider'
/ServiceProviders/{ServiceProviderSid}:
parameters:
- name: ServiceProviderSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
summary: delete a service provider
operationId: deleteServiceProvider
responses:
200:
description: service provider successfully deleted
404:
description: service provider not found
409:
description: service provider with active accounts can not be deleted
get:
summary: retrieve service provider
operationId: getServiceProvider
responses:
200:
description: service provider found
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceProvider'
404:
description: service provider not found
put:
summary: update service provider
operationId: updateServiceProvider
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceProvider'
responses:
200:
description: service provider updated
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceProvider'
404:
description: service provider not found
/ServiceProviders/{ServiceProviderSid}/Accounts:
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
post:
summary: create an account
operationId: createAccount
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: account name
example: foobar
sipRealm:
type: string
description: sip realm for registration
example: sip.mycompany.com
registrationUrl:
type: string
format: url
description: authentication webhook for registration
example: https://mycompany.com
required:
- name
responses:
201:
description: account successfully created
content:
application/json:
schema:
required:
- accountSid
properties:
accountSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
409:
description: account with this name already exists
callbacks:
onRegistrationAttempt:
'{$request.body#/registrationUrl}/auth':
post:
requestBody:
description: |
provides details of the authentication request. The receiving server is responsible for authenticating the
request as per [RFC 2617](https://tools.ietf.org/html/rfc2617)
content:
application/json:
schema:
required:
- method
- realm
- username
- expires
- nonce
- uri
- response
type: object
properties:
method:
type: string
description: sip request method
example: REGISTER
realm:
type: string
description: sip realm
example: mycompany.com
username:
type: string
description: sip username provided
example: daveh
expires:
type: number
description: expiration requested, in seconds
example: 3600
nonce:
type: string
description: nonce value
example: InFriVGWVoKeCckYrTx7wg=="
uri:
type: string
format: uri
description: sip uri in request
example: sip:mycompany.com
algorithm:
type: string
description: encryption algorithm used, default to MD5 if not provided
example: MD5
qop:
type: string
description: qop value
example: auth
cnonce:
type: string
description: cnonce value
example: 03d8d2aafd5a975f2b07dc90fe5f4100
nc:
type: string
description: nc value
example: 00000001
response:
type: string
description: digest value calculated by the client
example: db7b7dbec7edc0c427c1708031f67cc6
responses:
'200':
description: |
Your callback should return this HTTP status code in all cases.
if the request was authenticated and you wish to admit
the client to the network, this is indicated by setting the 'response'
attribute in the body to 'ok'
content:
application/json:
schema:
type: object
required:
- response
properties:
response:
type: string
description: indicates whether the request was successfully authenticated
enum:
- ok
- failed
example: ok
message:
type: string
description: a human-readable message
example: authentication granted
expires:
type: number
description: |
The expires value to grant to the requesting user.
If not provided, the expires value in the request is observed.
If provided, must be less than the requested expires value.
exileDuration:
type: number
description: |
If provided, represents a period in seconds during which the source IP
address will be blacklisted by the platform.
get:
summary: list accounts for a service provider
operationId: listAccounts
responses:
200:
description: list of accounts
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Account'
/ServiceProviders/{ServiceProviderSid}/Accounts/{AccountSid}:
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
delete:
summary: delete an account
operationId: deleteAccount
responses:
200:
description: account successfully deleted
404:
description: account not found
409:
description: account with applications or phone numbers can not be deleted
/VoipCarriers:
post:
summary: create voip carrier
operationId: createVoipCarrier
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: voip carrier name
description:
type: string
required:
- name
responses:
201:
description: voip carrier successfully created
content:
application/json:
schema:
required:
- voipCarrierSid
properties:
accountSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
409:
description: an existing voip carrier already exists with this name
get:
summary: list voip carriers
operationId: listVoipCarriers
responses:
200:
description: list of voip carriers
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/VoipCarrier'
/VoipCarriers/{VoipCarrierSid}:
parameters:
- name: VoipCarrierSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
summary: delete a voip carrier
operationId: deleteVoipCarrier
responses:
200:
description: voip carrier successfully deleted
404:
description: voip carrier not found
409:
description: voip carrier with active phone numbers can not be deleted
get:
summary: retrieve voip carrier
operationId: getVoipCarrier
responses:
200:
description: voip carrier found
content:
application/json:
schema:
$ref: '#/components/schemas/VoipCarrier'
404:
description: voip carrier not found
put:
summary: update voip carrier
operationId: updateVoipCarrier
parameters:
- name: VoipCarrierSid
in: path
required: true
style: simple
explode: false
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/VoipCarrier'
responses:
200:
description: voip carrier updated
content:
application/json:
schema:
$ref: '#/components/schemas/VoipCarrier'
404:
description: voip carrier not found
/VoipCarriers/{VoipCarrierSid}/PhoneNumbers:
parameters:
- name: VoipCarrierSid
in: path
required: true
schema:
type: string
format: uuid
post:
summary: provision a phone number into inventory from a Voip Carrier
operationId: provisionPhoneNumber
requestBody:
content:
application/json:
schema:
type: object
properties:
number:
type: string
description: telephone number
description:
type: string
required:
- number
responses:
201:
description: phone number successfully provisioned into inventory
content:
application/json:
schema:
required:
- phoneNumberSid
properties:
phoneNumberSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
409:
description: the specified phone number already exists in inventory
get:
summary: list phone numbers for a carrier
operationId: listProvisionedPhoneNumbers
responses:
200:
description: list of phone numbers
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PhoneNumber'
/Accounts/{AccountSid}/Applications:
post:
summary: create application
operationId: createApplication
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: application name
description:
type: string
statusUrl:
type: string
format: url
description: webhook to pass call status updates to
voiceUrl:
type: string
format: url
description: webhook to call when call is received
voiceFallbackUrl:
type: string
format: url
description: fallback webook url
required:
- name
- statusUrl
- voiceUrl
responses:
201:
description: application successfully created
content:
application/json:
schema:
required:
- ApplicationSid
properties:
accountSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
409:
description: an existing application already exists with this name
get:
summary: list applications for an account
operationId: listApplications
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
responses:
200:
description: retrieve applications for the account
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Application'
/Accounts/{AccountSid}/Applications/{ApplicationSid}:
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
- name: ApplicationSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
summary: delete an application
operationId: deleteApplication
responses:
200:
description: application successfully deleted
404:
description: application not found
get:
summary: retrieve application
operationId: getApplication
responses:
200:
description: application found
content:
application/json:
schema:
$ref: '#/components/schemas/Application'
404:
description: application not found
put:
summary: update application
operationId: updateApplication
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
- name: ApplicationSid
in: path
required: true
style: simple
explode: false
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Application'
responses:
200:
description: application updated
content:
application/json:
schema:
$ref: '#/components/schemas/Application'
404:
description: application not found
/Accounts/{AccountSid}/Applications/{ApplicationSid}/PhoneNumbers/{PhoneNumberSid}:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
- name: ApplicationSid
in: path
required: true
schema:
type: string
format: uuid
- name: PhoneNumberSid
in: path
required: true
schema:
type: string
format: uuid
delete:
summary: remove a phone number from an application
operationId: removePhoneNumberFromApplication
responses:
200:
description: phone number removed from this application
404:
description: phone number not found or was not assigned to this application
put:
summary: provision a phone number for an Application
operationId: assignPhoneNumberToApplication
responses:
200:
description: phone number successfully assigned to application
content:
application/json:
schema:
$ref: '#/components/schemas/PhoneNumber'
400:
description: bad request
409:
description: the specified phone number is already assigned to another application or account
/Accounts/{AccountSid}/Apikeys:
post:
summary: create api key
operationId: createApikey
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
responses:
201:
description: api key successfully created
content:
application/json:
schema:
required:
- apiKey
- apiKeySid
properties:
apiKeySid:
type: string
description: system identifier for api key that was created
format: uuid
example: 7531328e-eb08-4eff-887e-84e648214872
apiKey:
type: string
description: api key
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
404:
description: Account not found
/Accounts/{AccountSid}/Apikeys/{ApiKeySid}:
delete:
summary: delete api key
operationId: deleteApiKey
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
- name: ApiKeySid
in: path
required: true
schema:
type: string
responses:
200:
description: api key deleted
404:
description: api key or account not found
/Accounts/{AccountSid}/Applications/{ApplicationSid}/Calls:
post:
summary: create a call
operationId: createCall
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
- name: ApplicationSid
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
required:
- from
- to
type: object
properties:
url:
type: string
description: |
The url of the web application to control this call.
If not provided, the url specified in the application will be used
example: https://mycompany.com/deliver-message.json
from:
type: string
description: The calling party number
example: 16172375089
to:
type: string
description: The telephone number or sip endpoint to call
example: 16172228000
recordingUrl:
type: string
format: url
description: A websocket url to stream the call audio to
example: wss://myserver.com
recordingMix:
type: string
description: whether to record either or both parties
enum:
- caller
- callee
- stereo
- mixed
example: stereo
statusCallback:
type: string
format: url
description: The url to send call status change events to
example: https://company.com/status
responses:
201:
description: call successfully created
content:
application/json:
schema:
required:
- callSid
properties:
callSid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: token
schemas:
ServiceProvider:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
name:
type: string
description:
type: string
VoipCarrier:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
name:
type: string
description:
type: string
Account:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
sipRealm:
type: string
registrationHook:
type: string
format: url
serviceProvider:
$ref: '#/components/schemas/ServiceProvider'
Application:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
name:
type: string
account:
$ref: '#/components/schemas/Account'
callHook:
type: string
format: url
callBackupHook:
type: string
format: url
callStatusChangeHook:
type: string
format: url
PhoneNumber:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
number:
type: string
voipCarrier:
$ref: '#/components/schemas/VoipCarrier'
account:
$ref: '#/components/schemas/Account'
application:
$ref: '#/components/schemas/Application'
RegisteredUser:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
username:
type: string
domain:
type: string
Call:
type: object
properties:
id:
type: integer
uuid:
type: string
format: uuid
application:
$ref: '#/components/schemas/Application'
parentCall:
$ref: '#/components/schemas/Call'
direction:
type: string
enum:
- inbound
- outbound
phoneNumber:
$ref: '#/components/schemas/PhoneNumber'
inboundUser:
$ref: '#/components/schemas/RegisteredUser'
outboundUser:
$ref: '#/components/schemas/RegisteredUser'
callingNumber:
type: string
calledNumber:
type: string
security:
- bearerAuth: []

6
lib/utils/scrub-ids.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = function(tuples) {
return tuples.map((t) => {
delete t.id;
return t;
});
};

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "jambones-api-server",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"config": "^3.2.4",
"express": "^4.17.1",
"mysql": "^2.17.1",
"passport": "^0.4.0",
"passport-http-bearer": "^1.0.1",
"pino": "^5.14.0",
"request": "^2.88.0",
"request-debug": "^0.2.0",
"swagger-ui-express": "^4.1.2",
"yamljs": "^0.3.0"
}
}