revamped db schema and implemented some simple api

This commit is contained in:
Dave Horton
2019-12-02 16:49:25 -05:00
parent 8c287f06df
commit 47bb642854
19 changed files with 1005 additions and 592 deletions

4
app.js
View File

@@ -19,5 +19,7 @@ app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use('/v1', passport.authenticate('bearer', { session: false }));
app.use('/', routes);
app.use((err, req, res, next) => {
req.status(500).json({msg: err.message});
});
app.listen(PORT);

View File

@@ -1,8 +1,6 @@
/* SQLEditor (MySQL (2))*/
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS `call_routes`;
DROP TABLE IF EXISTS `conference_participants`;
@@ -17,13 +15,13 @@ 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 `registrations`;
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS `accounts`;
@@ -33,144 +31,92 @@ 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 ,
`application_sid` 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`)
`account_sid` CHAR(36) NOT NULL,
`call_hook` VARCHAR(255) NOT NULL,
`call_status_hook` VARCHAR(255) NOT NULL,
PRIMARY KEY (`application_sid`)
) 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,
`call_route_sid` CHAR(36) NOT NULL UNIQUE ,
`account_sid` CHAR(36) NOT NULL,
`regex` VARCHAR(255) NOT NULL,
`application_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
`application_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`call_route_sid`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `conferences`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`conference_sid` 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`)
`conference_participant_sid` CHAR(36) NOT NULL UNIQUE ,
`call_sid` CHAR(36),
`conference_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`conference_participant_sid`)
) 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 ,
`phone_number_sid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`account_id` INTEGER(10) UNSIGNED NOT NULL,
`application_id` INTEGER(10) UNSIGNED,
`account_sid` CHAR(36) NOT NULL,
`application_sid` CHAR(36),
`phone_number_inventory_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`phone_number_sid`)
) 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 ,
`queue_sid` 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`
CREATE TABLE IF NOT EXISTS `registrations`
(
`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 ,
`registration_sid` 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`)
PRIMARY KEY (`registration_sid`)
) ENGINE=InnoDB COMMENT='An active sip registration';
CREATE TABLE IF NOT EXISTS `queue_members`
(
`queue_member_sid` CHAR(36) NOT NULL UNIQUE ,
`call_sid` CHAR(36),
`queue_sid` CHAR(36) NOT NULL,
`position` INTEGER,
PRIMARY KEY (`queue_member_sid`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a queue that it is waiting';
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,
`call_sid` CHAR(36) NOT NULL UNIQUE ,
`parent_call_sid` CHAR(36),
`application_sid` CHAR(36),
`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,
`phone_number_sd` CHAR(36),
`inbound_user_sid` CHAR(36),
`outbound_user_sid` CHAR(36),
`calling_number` VARCHAR(255),
`called_number` VARCHAR(255),
`caller_name` VARCHAR(255),
@@ -189,96 +135,112 @@ CREATE TABLE IF NOT EXISTS `calls`
`dest_address` VARCHAR(255),
`dest_port` INTEGER UNSIGNED,
`url` VARCHAR(255),
PRIMARY KEY (`id`)
PRIMARY KEY (`call_sid`)
) 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 ,
`service_provider_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
`description` VARCHAR(255),
PRIMARY KEY (`id`)
PRIMARY KEY (`service_provider_sid`)
) ENGINE=InnoDB COMMENT='An organization that provides communication services to its ';
CREATE TABLE IF NOT EXISTS `accounts`
CREATE TABLE IF NOT EXISTS `api_keys`
(
`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';
`api_key_sid` CHAR(36) NOT NULL UNIQUE ,
`token` CHAR(36) NOT NULL UNIQUE ,
`account_sid` CHAR(36),
`service_provider_sid` CHAR(36),
PRIMARY KEY (`api_key_sid`)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
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,
`subscription_sid` CHAR(36) NOT NULL UNIQUE ,
`registration_sid` CHAR(36) NOT NULL,
`event` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An active sip subscription';
CREATE TABLE IF NOT EXISTS `accounts`
(
`account_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL,
`sip_realm` VARCHAR(255),
`service_provider_sid` CHAR(36) NOT NULL,
`registration_hook` VARCHAR(255),
PRIMARY KEY (`account_sid`)
) ENGINE=InnoDB COMMENT='A single end-user of the platform';
CREATE TABLE IF NOT EXISTS `voip_carriers`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`uuid` CHAR(36) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (`id`)
`description` VARCHAR(255),
PRIMARY KEY (`voip_carrier_sid`)
) 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 ,
`phone_number_inventory_sid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`voip_carrier_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
`voip_carrier_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`phone_number_inventory_sid`)
) 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_application_sid_idx` ON `applications` (`application_sid`);
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 `applications` ADD FOREIGN KEY account_sid_idxfk (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `call_routes` ADD FOREIGN KEY account_id_idxfk_2 (`account_id`) REFERENCES `accounts` (`id`);
CREATE INDEX `call_routes_call_route_sid_idx` ON `call_routes` (`call_route_sid`);
ALTER TABLE `call_routes` ADD FOREIGN KEY account_sid_idxfk_1 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `call_routes` ADD FOREIGN KEY application_id_idxfk (`application_id`) REFERENCES `applications` (`id`);
ALTER TABLE `call_routes` ADD FOREIGN KEY application_sid_idxfk (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY call_id_idxfk (`call_id`) REFERENCES `calls` (`id`);
CREATE INDEX `conferences_conference_sid_idx` ON `conferences` (`conference_sid`);
CREATE INDEX `conference_participants_conference_participant_sid_idx` ON `conference_participants` (`conference_participant_sid`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY call_sid_idxfk (`call_sid`) REFERENCES `calls` (`call_sid`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_id_idxfk (`conference_id`) REFERENCES `conferences` (`id`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_sid_idxfk (`conference_sid`) REFERENCES `conferences` (`conference_sid`);
ALTER TABLE `old_call` ADD FOREIGN KEY parent_call_id_idxfk (`parent_call_id`) REFERENCES `old_call` (`id`);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
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 application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_id_idxfk_3 (`account_id`) REFERENCES `accounts` (`id`);
CREATE INDEX `queues_queue_sid_idx` ON `queues` (`queue_sid`);
CREATE INDEX `registrations_registration_sid_idx` ON `registrations` (`registration_sid`);
CREATE INDEX `queue_members_queue_member_sid_idx` ON `queue_members` (`queue_member_sid`);
ALTER TABLE `queue_members` ADD FOREIGN KEY call_sid_idxfk_1 (`call_sid`) REFERENCES `calls` (`call_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_id_idxfk_2 (`application_id`) REFERENCES `applications` (`id`);
ALTER TABLE `queue_members` ADD FOREIGN KEY queue_sid_idxfk (`queue_sid`) REFERENCES `queues` (`queue_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY phone_number_inventory_id_idxfk (`phone_number_inventory_id`) REFERENCES `phone_number_inventory` (`id`);
CREATE INDEX `calls_call_sid_idx` ON `calls` (`call_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_sid_idxfk (`parent_call_sid`) REFERENCES `calls` (`call_sid`);
ALTER TABLE `queue_members` ADD FOREIGN KEY call_id_idxfk_1 (`call_id`) REFERENCES `calls` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE `queue_members` ADD FOREIGN KEY queue_id_idxfk (`queue_id`) REFERENCES `queues` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY phone_number_sd_idxfk (`phone_number_sd`) REFERENCES `phone_numbers` (`phone_number_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_id_idxfk_1 (`parent_call_id`) REFERENCES `calls` (`id`);
ALTER TABLE `calls` ADD FOREIGN KEY inbound_user_sid_idxfk (`inbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
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`);
ALTER TABLE `calls` ADD FOREIGN KEY outbound_user_sid_idxfk (`outbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY service_provider_sid_idxfk (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
ALTER TABLE `subscriptions` ADD FOREIGN KEY registration_sid_idxfk (`registration_sid`) REFERENCES `registrations` (`registration_sid`);
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`);
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 `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
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`);
CREATE INDEX `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `phone_number_inventory_sid_idx` ON `phone_number_inventory` (`phone_number_inventory_sid`);
ALTER TABLE `phone_number_inventory` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);

BIN
db/jambones.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
const Strategy = require('passport-http-bearer').Strategy;
const {getMysqlConnection} = require('../db');
const sql = `
SELECT api_keys.uuid, accounts.uuid
SELECT *
FROM api_keys
LEFT JOIN accounts
ON api_keys.account_id = accounts.id`;
WHERE api_keys.token = ?`;
function makeStrategy(logger) {
return new Strategy(
@@ -15,7 +14,7 @@ function makeStrategy(logger) {
logger.error(err, 'Error retrieving mysql connection');
return done(err);
}
conn.query({sql, nestTables: '_'}, [token], (err, results, fields) => {
conn.query(sql, [token], (err, results, fields) => {
conn.release();
if (err) {
logger.error(err, 'Error querying for api key');
@@ -28,9 +27,20 @@ function makeStrategy(logger) {
}
// found api key
return done(null,
{accountSid: results[0].accounts_uuid},
{scope: results[0].accounts_uuid ? ['user'] : ['admin']});
logger.info(results, 'api key lookup');
const user = {
account_sid: results[0].account_sid,
service_provider_sid: results[0].service_provider_sid,
isAdmin: results[0].account_sid === null && results[0].service_provider_sid === null,
isServiceProvider: results[0].service_provider_sid !== null,
isUser: results[0].account_sid != null
};
const scope = [];
if (user.isAdmin) scope.push('admin');
else if (user.isServiceProvider) scope.push('service_provider');
else scope.push('user');
logger.info(user, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});
});
});

31
lib/models/api-key.js Normal file
View File

@@ -0,0 +1,31 @@
const Model = require('./model');
class ApiKey extends Model {
constructor() {
super();
}
}
ApiKey.table = 'api_keys';
ApiKey.fields = [
{
name: 'api_key_sid',
type: 'string',
primaryKey: true
},
{
name: 'token',
type: 'string',
required: true
},
{
name: 'account_sid',
type: 'string'
},
{
name: 'service_provider_sid',
type: 'string'
}
];
module.exports = ApiKey;

141
lib/models/model.js Normal file
View File

@@ -0,0 +1,141 @@
const Emitter = require('events');
const uuidv4 = require('uuid/v4');
const assert = require('assert');
const {getMysqlConnection} = require('../db');
const {DbErrorBadRequest} = require('../utils/errors');
class Model extends Emitter {
constructor() {
super();
}
static getPrimaryKey() {
return this.fields.find((f) => f.primaryKey === true);
}
/**
* check validity of object to be inserted into db
*/
static checkIsInsertable(obj) {
// check all required fields are present
const required = this.fields.filter((f) => f.required === true);
const missing = required.find((f) => !(f.name in obj));
if (missing) throw new DbErrorBadRequest(`missing field ${missing.name}`);
return true;
}
/**
* insert object into the database
*/
static make(obj) {
return new Promise((resolve, reject) => {
const pk = this.getPrimaryKey();
const uuid = uuidv4();
obj[pk.name] = uuid;
this.checkIsInsertable(obj);
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`INSERT into ${this.table} SET ?`,
obj,
(err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(uuid);
});
});
});
}
/**
* delete object from database
*/
static remove(uuid) {
return new Promise((resolve, reject) => {
const pk = this.getPrimaryKey();
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`DELETE from ${this.table} WHERE ${pk.name} = ?`, uuid, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results.affectedRows);
});
});
});
}
/**
* retrieve all objects
*/
static retrieveAll() {
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`SELECT * from ${this.table}`, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* retrieve a specific object
*/
static retrieve(sid) {
return new Promise((resolve, reject) => {
const pk = this.getPrimaryKey();
assert.ok(pk, 'field definitions must include the primary key');
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`SELECT * from ${this.table} WHERE ${pk.name} = ?`, sid, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* update an object
*/
static update(sid, obj) {
const pk = this.getPrimaryKey();
assert.ok(pk, 'field definitions must include the primary key');
return new Promise((resolve, reject) => {
if (pk.name in obj) throw new DbErrorBadRequest(`primary key ${pk.name} is immutable`);
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`UPDATE ${this.table} SET ? WHERE ${pk.name} = '${sid}'`, obj, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results.affectedRows);
});
});
});
}
static getForeignKeyReferences(fk, sid) {
return new Promise((resolve, reject) => {
const arr = /(.*)\.(.*)/.exec(fk);
assert.ok(arr, `foreign key must be written as table.column: ${fk}`);
const table = arr[1];
const column = arr[2];
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`SELECT COUNT(*) as count from ${table} WHERE ${column} = ?`,
sid, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results[0].count);
});
});
});
}
}
Model.table = 'subclassResponsibility';
Model.fields = [];
module.exports = Model;

View File

@@ -1,23 +1,27 @@
const Emitter = require('events');
const {getMysqlConnection} = require('../db');
const scrubIds = require('../utils/scrub-ids');
const Model = require('./model');
class ServiceProvider extends Emitter {
class ServiceProvider extends Model {
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));
});
});
});
}
}
ServiceProvider.table = 'service_providers';
ServiceProvider.fields = [
{
name: 'service_provider_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'description',
type: 'string'
}
];
module.exports = ServiceProvider;

View File

@@ -0,0 +1,80 @@
const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const ApiKey = require('../../models/api-key');
const {isAdmin, isServiceProvider, isUser} = require('../../utils/scopes');
const decorate = require('./decorate');
const uuidv4 = require('uuid/v4');
const assert = require('assert');
const sysError = require('./error');
const preconditions = {
'add': validateAddToken,
'delete': validateDeleteToken
};
/**
* if user scope, add to the associated account
* if admin scope, only admin-level tokens may be created
*/
function validateAddToken(req) {
if (isAdmin(req) && ('account_sid' in req.body)) {
throw new DbErrorBadRequest('admin users may not create account-level tokens');
}
else if (isServiceProvider(req) && (!('account_sid' in req.body) && !('service_provider_sid' in req.body))) {
req.body['service_provider_sid'] = req.user.service_provider_sid
}
else if (isUser(req)) {
delete req.body['service_provider_sid'];
req.body['account_sid'] = req.user.account_sid;
}
req.body.token = uuidv4();
}
/**
* admin users can only delete admin tokens or service provider tokens
* service_provider users can delete service provider or user tokens
* user-scope may only delete their own tokens
*/
async function validateDeleteToken(req, sid) {
const results = await ApiKey.retrieve(sid);
if (0 == results.length) return;
if (isAdmin(req)) {
if (results[0].account_sid) {
throw new DbErrorBadRequest('an admin user may not delete account level api keys');
}
}
else if (isServiceProvider(req)) {
if (results[0].service_provider_sid === null && results[0].account_sid === null) {
throw new DbErrorBadRequest('a service provider user may not delete an admin token');
}
if (results[0].service_provider_sid && results[0].service_provider_sid != req.user.service_provider_sid) {
throw new DbErrorBadRequest('a service provider user may not delete api key from another service provider');
}
}
else if (isUser(req)) {
if (results[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('a user may not delete a token associated with a different account');
}
}
}
/**
* need to handle here because response is slightly different than standard for an insert
* (returning the token generated along with the sid)
*/
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
if ('add' in preconditions) {
assert(typeof preconditions.add === 'function');
await preconditions.add(req);
}
const uuid = await ApiKey.make(req.body);
res.status(201).json({sid: uuid, token: req.body.token});
} catch (err) {
sysError(logger, res, err);
}
});
decorate(router, ApiKey, ['delete'], preconditions);
module.exports = router;

101
lib/routes/api/decorate.js Normal file
View File

@@ -0,0 +1,101 @@
const assert = require('assert');
const sysError = require('./error');
module.exports = decorate;
const decorators = {
'list': list,
'add': add,
'retrieve': retrieve,
'update': update,
'delete': remove
};
function decorate(router, klass, methods, preconditions) {
const decs = methods && Array.isArray(methods) && methods[0] !== '*' ? methods : Object.keys(decorators);
decs.forEach((m) => {
assert(m in decorators);
decorators[m](router, klass, preconditions);
});
}
function list(router, klass) {
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 klass.retrieveAll();
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
}
function add(router, klass, preconditions) {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
if ('add' in preconditions) {
assert(typeof preconditions.add === 'function');
await preconditions.add(req);
}
const uuid = await klass.make(req.body);
res.status(201).json({sid: uuid});
} catch (err) {
sysError(logger, res, err);
}
});
}
function retrieve(router, klass) {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await klass.retrieve(req.params.sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
catch (err) {
sysError(logger, res, err);
}
});
}
function update(router, klass) {
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const rowsAffected = await klass.update(sid, req.body);
if (rowsAffected === 0) {
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
}
function remove(router, klass, preconditions) {
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
if ('delete' in preconditions) {
assert(typeof preconditions.delete === 'function');
await preconditions.delete(req, sid);
}
const rowsAffected = await klass.remove(sid);
if (rowsAffected === 0) {
logger.info(`unable to delete ${klass.name} with sid ${sid}: not found`);
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
}

20
lib/routes/api/error.js Normal file
View File

@@ -0,0 +1,20 @@
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
function sysError(logger, res, err) {
if (err instanceof DbErrorBadRequest) {
logger.error(err, 'invalid client request');
return res.status(400).json({msg: err.message});
}
if (err instanceof DbErrorUnprocessableRequest) {
logger.error(err, 'unprocessable request');
return res.status(422).json({msg: err.message});
}
if (err.message.includes('ER_DUP_ENTRY')) {
logger.error(err, 'duplicate entry on insert');
return res.status(422).json({msg: err.message});
}
logger.error(err, 'Database error');
res.status(500).json({msg: err.message});
}
module.exports = sysError;

View File

@@ -1,21 +1,15 @@
const api = require('express').Router();
const {isAdmin} = require('../../utils/scopes');
function isAdmin(req, res, next) {
if (req.authInfo.scope.includes('admin')) return next();
function isAdminScope(req, res, next) {
if (isAdmin(req)) 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'));
api.use('/ServiceProviders', isAdminScope, require('./service-providers'));
api.use('/ApiKeys', require('./api-keys'));
module.exports = api;

View File

@@ -1,28 +1,17 @@
const router = require('express').Router();
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const ServiceProvider = require('../../models/service-provider');
const decorate = require('./decorate');
const preconditions = {
'delete': noActiveAccounts
};
function sysError(logger, res, err) {
logger.error(err, 'Database error');
res.status(500).end();
/* can not delete a service provider if it has any active accounts */
async function noActiveAccounts(req, sid) {
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
}
/* 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) => {
});
decorate(router, ServiceProvider, ['*'], preconditions);
module.exports = router;

View File

@@ -12,6 +12,58 @@ servers:
- url: /v1
description: development server
paths:
/Apikeys:
post:
summary: create an api key
operationId: createApikey
requestBody:
content:
application/json:
schema:
type: object
properties:
service_provider_sid:
type: string
description: service provider scope for the generated api key
account_sid:
type: string
description: account scope for the generated api key
responses:
201:
description: api key successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulApiKeyAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Apikeys/{ApiKeySid}:
parameters:
- name: ApiKeySid
in: path
required: true
schema:
type: string
delete:
summary: delete api key
operationId: deleteApiKey
responses:
200:
description: api key deleted
404:
description: api key or account not found
/ServiceProviders:
post:
summary: create service provider
@@ -35,17 +87,14 @@ paths:
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
$ref: '#/components/schemas/SuccessfulAdd'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: list service providers
operationId: listServiceProviders
@@ -71,12 +120,16 @@ paths:
summary: delete a service provider
operationId: deleteServiceProvider
responses:
200:
204:
description: service provider successfully deleted
404:
description: service provider not found
409:
description: service provider with active accounts can not be deleted
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve service provider
operationId: getServiceProvider
@@ -98,14 +151,16 @@ paths:
schema:
$ref: '#/components/schemas/ServiceProvider'
responses:
200:
204:
description: service provider updated
404:
description: service provider not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceProvider'
404:
description: service provider not found
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/Accounts:
parameters:
- name: ServiceProviderSid
@@ -640,8 +695,8 @@ paths:
/Accounts/{AccountSid}/Apikeys:
post:
summary: create api key
operationId: createApikey
summary: create an account level api key
operationId: createAccountApikey
parameters:
- name: AccountSid
in: path
@@ -660,22 +715,22 @@ paths:
- apiKey
- apiKeySid
properties:
apiKeySid:
api_key_sid:
type: string
description: system identifier for api key that was created
format: uuid
example: 7531328e-eb08-4eff-887e-84e648214872
apiKey:
token:
type: string
description: api key
description: api key authorization token
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
404:
description: Account not found
/Accounts/{AccountSid}/Apikeys/{ApiKeySid}:
delete:
summary: delete api key
operationId: deleteApiKey
summary: delete account-level api key
operationId: deleteAccountApiKey
parameters:
- name: AccountSid
in: path
@@ -775,121 +830,171 @@ components:
scheme: bearer
bearerFormat: token
schemas:
SuccessfulApiKeyAdd:
type: object
required:
- sid
- token
properties:
sid:
type: string
token:
type: string
example:
sid: 9d26a637-1679-471f-8da8-7150266e1254
token: 589cead6-de24-4689-8ac3-08ffaf102811
SuccessfulAdd:
type: object
required:
- sid
properties:
sid:
type: string
example:
sid: 9d26a637-1679-471f-8da8-7150266e1254
GeneralError:
type: object
required:
- msg
properties:
msg:
type: string
example:
msg: cannot delete service provider with active accounts
ServiceProvider:
type: object
properties:
id:
type: integer
uuid:
service_provider_sid:
type: string
format: uuid
name:
type: string
description:
type: string
required:
- service_provider_sid
- name
VoipCarrier:
type: object
properties:
id:
type: integer
uuid:
voip_carrier_sid:
type: string
format: uuid
name:
type: string
description:
type: string
required:
- voip_carrier_sid
- name
Account:
type: object
properties:
id:
type: integer
uuid:
account_sid:
type: string
format: uuid
sipRealm:
name:
type: string
registrationHook:
sip_realm:
type: string
registration_hook:
type: string
format: url
serviceProvider:
service_provider:
$ref: '#/components/schemas/ServiceProvider'
required:
- account_sid
- name
- service_provider
Application:
type: object
properties:
id:
type: integer
uuid:
application_sid:
type: string
format: uuid
name:
type: string
account:
$ref: '#/components/schemas/Account'
callHook:
call_hook:
type: string
format: url
callBackupHook:
type: string
format: url
callStatusChangeHook:
call_status_hook:
type: string
format: url
required:
- application_sid
- name
- account
- call_hook
- call_status_hook
PhoneNumber:
type: object
properties:
id:
type: integer
uuid:
phone_number_sid:
type: string
format: uuid
number:
type: string
voipCarrier:
voip_carrier:
$ref: '#/components/schemas/VoipCarrier'
account:
$ref: '#/components/schemas/Account'
application:
$ref: '#/components/schemas/Application'
RegisteredUser:
required:
- phone_number_sid
- number
- voip_carrier
- account
Registration:
type: object
properties:
id:
type: integer
uuid:
registration_sid:
type: string
format: uuid
username:
type: string
domain:
type: string
sip_contact:
type: string
sip_user_agent:
type: string
required:
- registration_sid
- username
- domain
- sip_contact
Call:
type: object
properties:
id:
type: integer
uuid:
call_sid:
type: string
format: uuid
application:
$ref: '#/components/schemas/Application'
parentCall:
parent_call:
$ref: '#/components/schemas/Call'
direction:
type: string
enum:
- inbound
- outbound
phoneNumber:
phone_number:
$ref: '#/components/schemas/PhoneNumber'
inboundUser:
inbound_user:
$ref: '#/components/schemas/RegisteredUser'
outboundUser:
outbound_user:
$ref: '#/components/schemas/RegisteredUser'
callingNumber:
calling_number:
type: string
calledNumber:
called_number:
type: string
required:
- call_sid
- application
- direction
security:
- bearerAuth: []

23
lib/utils/errors.js Normal file
View File

@@ -0,0 +1,23 @@
class DbError extends Error {
constructor(msg) {
super(msg);
}
}
class DbErrorBadRequest extends DbError {
constructor(msg) {
super(msg);
}
}
class DbErrorUnprocessableRequest extends DbError {
constructor(msg) {
super(msg);
}
}
module.exports = {
DbError,
DbErrorBadRequest,
DbErrorUnprocessableRequest
};

17
lib/utils/scopes.js Normal file
View File

@@ -0,0 +1,17 @@
function isAdmin(req) {
return req.authInfo.scope.includes('admin');
}
function isServiceProvider(req) {
return req.authInfo.scope.includes('service_provider');
}
function isUser(req) {
return req.authInfo.scope.includes('user');
}
module.exports = {
isAdmin,
isServiceProvider,
isUser
};

View File

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

23
lib/utils/transforms.js Normal file
View File

@@ -0,0 +1,23 @@
function scrubIds(tuples) {
return tuples.map((t) => {
delete t.id;
return t;
});
}
function rewriteKeys(tuples, obj) {
return tuples.map((t) => {
Object.keys(obj).forEach((k) => {
if (k in t) {
t[obj[k]] = t[k];
delete t[k];
}
});
});
}
module.exports = {
scrubIds,
rewriteKeys
};

View File

@@ -19,6 +19,7 @@
"request": "^2.88.0",
"request-debug": "^0.2.0",
"swagger-ui-express": "^4.1.2",
"uuid": "^3.3.3",
"yamljs": "^0.3.0"
}
}