From 8c287f06df77428a5e493cc9b288b2e1e4d67c1d Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Wed, 27 Nov 2019 09:23:14 -0500 Subject: [PATCH] initial checkin --- .eslintignore | 1 + .eslintrc.json | 126 +++ .gitignore | 41 + README.md | 1 + app.js | 23 + config/default.json.example | 12 + db/jambones-sql.sql | 284 +++++++ db/jambones.sqs | 1138 +++++++++++++++++++++++++++ lib/auth/index.js | 39 + lib/db/index.js | 5 + lib/db/mysql.js | 13 + lib/models/service-provider.js | 23 + lib/routes/api/index.js | 21 + lib/routes/api/service-providers.js | 28 + lib/routes/index.js | 23 + lib/swagger/swagger.yaml | 895 +++++++++++++++++++++ lib/utils/scrub-ids.js | 6 + package.json | 24 + 18 files changed, 2703 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 config/default.json.example create mode 100644 db/jambones-sql.sql create mode 100644 db/jambones.sqs create mode 100644 lib/auth/index.js create mode 100644 lib/db/index.js create mode 100644 lib/db/mysql.js create mode 100644 lib/models/service-provider.js create mode 100644 lib/routes/api/index.js create mode 100644 lib/routes/api/service-providers.js create mode 100644 lib/routes/index.js create mode 100644 lib/swagger/swagger.yaml create mode 100644 lib/utils/scrub-ids.js create mode 100644 package.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ab1cfb4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +test/* diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d81ce6d --- /dev/null +++ b/.eslintrc.json @@ -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 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29c01a0 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..71029b2 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# jambones-api-server \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..ad628aa --- /dev/null +++ b/app.js @@ -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); diff --git a/config/default.json.example b/config/default.json.example new file mode 100644 index 0000000..21cc8b8 --- /dev/null +++ b/config/default.json.example @@ -0,0 +1,12 @@ +{ + "logging": { + "level": "info" + }, + "mysql": { + "host": "example.org", + "user": "bob", + "password": "secret", + "database": "jambones", + "connectionLimit": 10 + } +} \ No newline at end of file diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql new file mode 100644 index 0000000..287ddc4 --- /dev/null +++ b/db/jambones-sql.sql @@ -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`); diff --git a/db/jambones.sqs b/db/jambones.sqs new file mode 100644 index 0000000..d058255 --- /dev/null +++ b/db/jambones.sqs @@ -0,0 +1,1138 @@ + + + + + + + + + 687.00 + 1004.00 + + + 282.00 + 140.00 + + 5 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1121.00 + 944.00 + + + 227.00 + 80.00 + + 7 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 84.00 + 36.00 + + + 227.00 + 80.00 + + 14 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1099.00 + 151.00 + + + 252.00 + 100.00 + + 1 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + id + accounts + + + 4 + 1 + + + + + + + + + + + + + + + + + + 80.00 + 181.00 + + + 257.00 + 100.00 + + 13 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + id + voip_carriers + + + 2 + 1 + + + + + + + + + + + + + + + + + 1109.00 + 355.00 + + + 252.00 + 120.00 + + 11 + + + + 1 + + + + + + + + + + + + + + + + + + + id + accounts + + + 3 + 1 + + + + + + + + + + + + + + + + id + applications + + + 4 + 1 + + + + + + + + + + + + + + + + + 681.00 + 817.00 + + + 254.00 + 120.00 + + 12 + + + + 1 + + + + + + + + + + + + + + + + + + id + calls + + + 2 + 1 + + + + + + + + + + + id + queues + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + + + + 664.00 + 148.00 + + + 283.00 + 140.00 + + 10 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + service_providers + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + 689.00 + 1190.00 + + + 283.00 + 100.00 + + 6 + + + + 1 + + + + + + + + + + + + + + + + + + id + registered_users + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + + + 75.00 + 672.00 + + + 306.00 + 640.00 + + 2 + + + + 1 + + + + + + + + + + + + + + + + + + id + calls + + + 4 + 2 + + + + + + + + + + + parent_call_id + calls + + + 4 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + phone_numbers + + + 4 + 2 + + + + + + + + + + id + registered_users + + + 4 + 2 + + + + + + + + + id + registered_users + + + 4 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 83.00 + 322.00 + + + 331.00 + 140.00 + + 8 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + id + accounts + + + 4 + 2 + + + + + + + + + + + id + applications + + + 4 + 2 + + + + + + + + + id + phone_number_inventory + + + 4 + 1 + + + + + + + + + + + + + + + + + + 683.00 + 684.00 + + + 254.00 + 100.00 + + 4 + + + + 1 + + + + + + + + + + + + + + + + + + id + calls + + + 2 + 1 + + + + + + + + + id + conferences + + + 4 + 1 + + + + + + + + + + + + + + + + + + 1116.00 + 807.00 + + + 227.00 + 80.00 + + 3 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 669.00 + 338.00 + + + 278.00 + 143.00 + + 0 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + id + accounts + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 675.00 + 16.00 + + + 227.00 + 100.00 + + 9 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/auth/index.js b/lib/auth/index.js new file mode 100644 index 0000000..72b00e4 --- /dev/null +++ b/lib/auth/index.js @@ -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; diff --git a/lib/db/index.js b/lib/db/index.js new file mode 100644 index 0000000..9d77afc --- /dev/null +++ b/lib/db/index.js @@ -0,0 +1,5 @@ +const getMysqlConnection = require('./mysql'); + +module.exports = { + getMysqlConnection +}; diff --git a/lib/db/mysql.js b/lib/db/mysql.js new file mode 100644 index 0000000..88c1ef7 --- /dev/null +++ b/lib/db/mysql.js @@ -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); diff --git a/lib/models/service-provider.js b/lib/models/service-provider.js new file mode 100644 index 0000000..f1c68ba --- /dev/null +++ b/lib/models/service-provider.js @@ -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; diff --git a/lib/routes/api/index.js b/lib/routes/api/index.js new file mode 100644 index 0000000..77f25b5 --- /dev/null +++ b/lib/routes/api/index.js @@ -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; diff --git a/lib/routes/api/service-providers.js b/lib/routes/api/service-providers.js new file mode 100644 index 0000000..ade6440 --- /dev/null +++ b/lib/routes/api/service-providers.js @@ -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; diff --git a/lib/routes/index.js b/lib/routes/index.js new file mode 100644 index 0000000..037c705 --- /dev/null +++ b/lib/routes/index.js @@ -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; diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml new file mode 100644 index 0000000..310227b --- /dev/null +++ b/lib/swagger/swagger.yaml @@ -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: [] \ No newline at end of file diff --git a/lib/utils/scrub-ids.js b/lib/utils/scrub-ids.js new file mode 100644 index 0000000..3daad62 --- /dev/null +++ b/lib/utils/scrub-ids.js @@ -0,0 +1,6 @@ +module.exports = function(tuples) { + return tuples.map((t) => { + delete t.id; + return t; + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7402451 --- /dev/null +++ b/package.json @@ -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" + } +}