diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js new file mode 100644 index 0000000..8c4636c --- /dev/null +++ b/src/backend/internal/dead-host.js @@ -0,0 +1,74 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const deadHostModel = require('../models/dead_host'); + +function omissions () { + return ['is_deleted']; +} + +const internalDeadHost = { + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('dead_hosts:list') + .then(access_data => { + let query = deadHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .orderBy('domain_name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('domain_name', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Integer} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = deadHostModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then(row => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalDeadHost; diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js new file mode 100644 index 0000000..56a978d --- /dev/null +++ b/src/backend/internal/proxy-host.js @@ -0,0 +1,279 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const proxyHostModel = require('../models/proxy_host'); + +function omissions () { + return ['is_deleted']; +} + +const internalProxyHost = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + let auth = data.auth || null; + delete data.auth; + + data.avatar = data.avatar || ''; + data.roles = data.roles || []; + + if (typeof data.is_disabled !== 'undefined') { + data.is_disabled = data.is_disabled ? 1 : 0; + } + + return access.can('proxy_hosts:create', data) + .then(() => { + data.avatar = gravatar.url(data.email, {default: 'mm'}); + + return userModel + .query() + .omit(omissions()) + .insertAndFetch(data); + }) + .then(user => { + if (auth) { + return authModel + .query() + .insert({ + user_id: user.id, + type: auth.type, + secret: auth.secret, + meta: {} + }) + .then(() => { + return user; + }); + } else { + return user; + } + }) + .then(user => { + // Create permissions row as well + let is_admin = data.roles.indexOf('admin') !== -1; + + return userPermissionModel + .query() + .insert({ + user_id: user.id, + visibility: is_admin ? 'all' : 'user', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + streams: 'manage', + access_lists: 'manage' + }) + .then(() => { + return internalProxyHost.get(access, {id: user.id, expand: ['permissions']}); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.email] + * @param {String} [data.name] + * @return {Promise} + */ + update: (access, data) => { + if (typeof data.is_disabled !== 'undefined') { + data.is_disabled = data.is_disabled ? 1 : 0; + } + + return access.can('proxy_hosts:update', data.id) + .then(() => { + + // Make sure that the user being updated doesn't change their email to another user that is already using it + // 1. get user we want to update + return internalProxyHost.get(access, {id: data.id}) + .then(user => { + + // 2. if email is to be changed, find other users with that email + if (typeof data.email !== 'undefined') { + data.email = data.email.toLowerCase().trim(); + + if (user.email !== data.email) { + return internalProxyHost.isEmailAvailable(data.email, data.id) + .then(available => { + if (!available) { + throw new error.ValidationError('Email address already in use - ' + data.email); + } + + return user; + }); + } + } + + // No change to email: + return user; + }); + }) + .then(user => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); + } + + data.avatar = gravatar.url(data.email || user.email, {default: 'mm'}); + + return userModel + .query() + .omit(omissions()) + .patchAndFetchById(user.id, data) + .then(saved_user => { + return _.omit(saved_user, omissions()); + }); + }) + .then(() => { + return internalProxyHost.get(access, {id: data.id}); + }); + }, + + /** + * @param {Access} access + * @param {Object} [data] + * @param {Integer} [data.id] Defaults to the token user + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + if (typeof data === 'undefined') { + data = {}; + } + + if (typeof data.id === 'undefined' || !data.id) { + data.id = access.token.get('attrs').id; + } + + return access.can('proxy_hosts:get', data.id) + .then(() => { + let query = userModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowEager('[permissions]') + .first(); + + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + query.omit(data.omit); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.eager('[' + data.expand.join(', ') + ']'); + } + + return query; + }) + .then(row => { + if (row) { + return _.omit(row, omissions()); + } else { + throw new error.ItemNotFoundError(data.id); + } + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access.can('proxy_hosts:delete', data.id) + .then(() => { + return internalProxyHost.get(access, {id: data.id}); + }) + .then(user => { + if (!user) { + throw new error.ItemNotFoundError(data.id); + } + + // Make sure user can't delete themselves + if (user.id === access.token.get('attrs').id) { + throw new error.PermissionError('You cannot delete yourself.'); + } + + return userModel + .query() + .where('id', user.id) + .patch({ + is_deleted: 1 + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('proxy_hosts:list') + .then(access_data => { + let query = proxyHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .orderBy('domain_name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('domain_name', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Integer} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = proxyHostModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then(row => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalProxyHost; diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js new file mode 100644 index 0000000..bb37e97 --- /dev/null +++ b/src/backend/internal/redirection-host.js @@ -0,0 +1,74 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const redirectionHostModel = require('../models/redirection_host'); + +function omissions () { + return ['is_deleted']; +} + +const internalProxyHost = { + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('redirection_hosts:list') + .then(access_data => { + let query = redirectionHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .orderBy('domain_name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('domain_name', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Integer} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = redirectionHostModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then(row => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalProxyHost; diff --git a/src/backend/internal/report.js b/src/backend/internal/report.js index da4dcc6..5124982 100644 --- a/src/backend/internal/report.js +++ b/src/backend/internal/report.js @@ -1,7 +1,11 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); +const _ = require('lodash'); +const error = require('../lib/error'); +const internalProxyHost = require('./proxy-host'); +const internalRedirectionHost = require('./redirection-host'); +const internalDeadHost = require('./dead-host'); +const internalStream = require('./stream'); const internalReport = { @@ -11,14 +15,27 @@ const internalReport = { */ getHostsReport: access => { return access.can('reports:hosts', 1) - .then(() => { + .then(access_data => { + let user_id = access.token.get('attrs').id; + + let promises = [ + internalProxyHost.getCount(user_id, access_data.visibility), + internalRedirectionHost.getCount(user_id, access_data.visibility), + internalStream.getCount(user_id, access_data.visibility), + internalDeadHost.getCount(user_id, access_data.visibility) + ]; + + return Promise.all(promises); + }) + .then(counts => { return { - proxy: 12, - redirection: 2, - stream: 1, - '404': 0 + proxy: counts.shift(), + redirection: counts.shift(), + stream: counts.shift(), + dead: counts.shift() }; }); + } }; diff --git a/src/backend/internal/stream.js b/src/backend/internal/stream.js new file mode 100644 index 0000000..7e30ac4 --- /dev/null +++ b/src/backend/internal/stream.js @@ -0,0 +1,74 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const streamModel = require('../models/stream'); + +function omissions () { + return ['is_deleted']; +} + +const internalStream = { + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('streams:list') + .then(access_data => { + let query = streamModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .orderBy('incoming_port', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('incoming_port', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Integer} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = streamModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then(row => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalStream; diff --git a/src/backend/lib/access.js b/src/backend/lib/access.js index 3bcda77..04bf196 100644 --- a/src/backend/lib/access.js +++ b/src/backend/lib/access.js @@ -1,11 +1,24 @@ 'use strict'; -const _ = require('lodash'); -const validator = require('ajv'); -const error = require('./error'); -const userModel = require('../models/user'); -const TokenModel = require('../models/token'); -const roleSchema = require('./access/roles.json'); +/** + * Some Notes: This is a friggin complicated piece of code. + * + * "scope" in this file means "where did this token come from and what is using it", so 99% of the time + * the "scope" is going to be "user" because it would be a user token. This is not to be confused with + * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. + * + * + */ + +const _ = require('lodash'); +const logger = require('../logger').access; +const validator = require('ajv'); +const error = require('./error'); +const userModel = require('../models/user'); +const proxyHostModel = require('../models/proxy_host'); +const TokenModel = require('../models/token'); +const roleSchema = require('./access/roles.json'); +const permsSchema = require('./access/permissions.json'); module.exports = function (token_string) { let Token = new TokenModel(); @@ -14,6 +27,7 @@ module.exports = function (token_string) { let object_cache = {}; let allow_internal_access = false; let user_roles = []; + let permissions = {}; /** * Loads the Token object from the token string @@ -28,7 +42,7 @@ module.exports = function (token_string) { reject(new error.PermissionError('Permission Denied')); } else { resolve(Token.load(token_string) - .then((data) => { + .then(data => { token_data = data; // At this point we need to load the user from the DB and make sure they: @@ -43,8 +57,10 @@ module.exports = function (token_string) { .where('id', token_data.attrs.id) .andWhere('is_deleted', 0) .andWhere('is_disabled', 0) - .first('id') - .then((user) => { + .allowEager('[permissions]') + .eager('[permissions]') + .first() + .then(user => { if (user) { // make sure user has all scopes of the token // The `user` role is not added against the user row, so we have to just add it here to get past this check. @@ -62,7 +78,9 @@ module.exports = function (token_string) { } else { initialised = true; user_roles = user.roles; + permissions = user.permissions; } + } else { throw new error.AuthError('User cannot be loaded for Token'); } @@ -99,6 +117,34 @@ module.exports = function (token_string) { resolve(token_user_id ? [token_user_id] : []); break; + // Proxy Hosts + case 'proxy_hosts': + let query = proxyHostModel + .query() + .select('id') + .andWhere('is_deleted', 0); + + if (permissions.visibility === 'user') { + query.andWhere('owner_user_id', token_user_id); + } + + resolve(query + .then(rows => { + let result = []; + _.forEach(rows, (rule_row) => { + result.push(rule_row.id); + }); + + // enum should not have less than 1 item + if (!result.length) { + result.push(0); + } + + return result; + }) + ); + break; + // DEFAULT: null default: resolve(null); @@ -121,7 +167,7 @@ module.exports = function (token_string) { /** * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema * - * @param {String} permission_label + * @param {String} permission_label * @returns {Object} */ this.getObjectSchema = permission_label => { @@ -207,9 +253,15 @@ module.exports = function (token_string) { .then(objectSchema => { let data_schema = { [permission]: { - data: data, - scope: Token.get('scope'), - roles: user_roles + data: data, + scope: Token.get('scope'), + roles: user_roles, + permission_visibility: permissions.visibility, + permission_proxy_hosts: permissions.proxy_hosts, + permission_redirection_hosts: permissions.redirection_hosts, + permission_dead_hosts: permissions.dead_hosts, + permission_streams: permissions.streams, + permission_access_lists: permissions.access_lists } }; @@ -223,9 +275,9 @@ module.exports = function (token_string) { permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json'); - //console.log('objectSchema:', JSON.stringify(objectSchema, null, 2)); - //console.log('permissionSchema:', JSON.stringify(permissionSchema, null, 2)); - //console.log('data_schema:', JSON.stringify(data_schema, null, 2)); + //logger.debug('objectSchema:', JSON.stringify(objectSchema, null, 2)); + //logger.debug('permissionSchema:', JSON.stringify(permissionSchema, null, 2)); + //logger.debug('data_schema:', JSON.stringify(data_schema, null, 2)); let ajv = validator({ verbose: true, @@ -236,17 +288,21 @@ module.exports = function (token_string) { coerceTypes: true, schemas: [ roleSchema, + permsSchema, objectSchema, permissionSchema ] }); - return ajv.validate('permissions', data_schema); + return ajv.validate('permissions', data_schema) + .then(() => { + return data_schema[permission]; + }); }); }) .catch(err => { - //console.log(err.message); - //console.log(err.errors); + //logger.error(err.message); + //logger.error(err.errors); throw new error.PermissionError('Permission Denied', err); }); diff --git a/src/backend/lib/access/dead_hosts-list.json b/src/backend/lib/access/dead_hosts-list.json new file mode 100644 index 0000000..925b52c --- /dev/null +++ b/src/backend/lib/access/dead_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/permissions.json b/src/backend/lib/access/permissions.json new file mode 100644 index 0000000..cf64a7d --- /dev/null +++ b/src/backend/lib/access/permissions.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "perms", + "definitions": { + "view": { + "type": "string", + "pattern": "^(view|manage)$" + }, + "manage": { + "type": "string", + "pattern": "^(manage)$" + } + } +} + diff --git a/src/backend/lib/access/proxy_hosts-list.json b/src/backend/lib/access/proxy_hosts-list.json new file mode 100644 index 0000000..10c4746 --- /dev/null +++ b/src/backend/lib/access/proxy_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/redirection_hosts-list.json b/src/backend/lib/access/redirection_hosts-list.json new file mode 100644 index 0000000..227fc54 --- /dev/null +++ b/src/backend/lib/access/redirection_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/streams-list.json b/src/backend/lib/access/streams-list.json new file mode 100644 index 0000000..3443aa8 --- /dev/null +++ b/src/backend/lib/access/streams-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/logger.js b/src/backend/logger.js index 0cbfb09..aeb4c70 100644 --- a/src/backend/logger.js +++ b/src/backend/logger.js @@ -3,5 +3,6 @@ const {Signale} = require('signale'); module.exports = { global: new Signale({scope: 'Global '}), migrate: new Signale({scope: 'Migrate '}), - express: new Signale({scope: 'Express '}) + express: new Signale({scope: 'Express '}), + access: new Signale({scope: 'Access '}) }; diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 03ee069..573b9c3 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -77,7 +77,6 @@ exports.up = function (knex/*, Promise*/) { table.integer('caching_enabled').notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0); table.json('meta').notNull(); - table.unique(['domain_name', 'is_deleted']); }); }) .then(() => { @@ -96,7 +95,6 @@ exports.up = function (knex/*, Promise*/) { table.string('ssl_provider').notNull().defaultTo(''); table.integer('block_exploits').notNull().unsigned().defaultTo(0); table.json('meta').notNull(); - table.unique(['domain_name', 'is_deleted']); }); }) .then(() => { @@ -112,7 +110,6 @@ exports.up = function (knex/*, Promise*/) { table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); table.string('ssl_provider').notNull().defaultTo(''); table.json('meta').notNull(); - table.unique(['domain_name', 'is_deleted']); }); }) .then(() => { @@ -130,7 +127,6 @@ exports.up = function (knex/*, Promise*/) { table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); table.json('meta').notNull(); - table.unique(['incoming_port', 'is_deleted']); }); }) .then(() => { diff --git a/src/backend/models/dead_host.js b/src/backend/models/dead_host.js new file mode 100644 index 0000000..26e9292 --- /dev/null +++ b/src/backend/models/dead_host.js @@ -0,0 +1,48 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class DeadHost extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'DeadHost'; + } + + static get tableName () { + return 'dead_host'; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'dead_host.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = DeadHost; diff --git a/src/backend/models/proxy_host.js b/src/backend/models/proxy_host.js new file mode 100644 index 0000000..488bc72 --- /dev/null +++ b/src/backend/models/proxy_host.js @@ -0,0 +1,48 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class ProxyHost extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'ProxyHost'; + } + + static get tableName () { + return 'proxy_host'; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'proxy_host.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = ProxyHost; diff --git a/src/backend/models/redirection_host.js b/src/backend/models/redirection_host.js new file mode 100644 index 0000000..7c5ab9d --- /dev/null +++ b/src/backend/models/redirection_host.js @@ -0,0 +1,48 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class RedirectionHost extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'RedirectionHost'; + } + + static get tableName () { + return 'redirection_host'; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'redirection_host.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = RedirectionHost; diff --git a/src/backend/models/stream.js b/src/backend/models/stream.js new file mode 100644 index 0000000..f002a6c --- /dev/null +++ b/src/backend/models/stream.js @@ -0,0 +1,48 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class Stream extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'Stream'; + } + + static get tableName () { + return 'stream'; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'stream.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = Stream; diff --git a/src/backend/routes/api/main.js b/src/backend/routes/api/main.js index 107047e..814a0fd 100644 --- a/src/backend/routes/api/main.js +++ b/src/backend/routes/api/main.js @@ -30,6 +30,10 @@ router.get('/', (req, res/*, next*/) => { router.use('/tokens', require('./tokens')); router.use('/users', require('./users')); router.use('/reports', require('./reports')); +router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); +router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); +router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); +router.use('/nginx/streams', require('./nginx/streams')); /** * API 404 for all other routes diff --git a/src/backend/routes/api/nginx/dead_hosts.js b/src/backend/routes/api/nginx/dead_hosts.js new file mode 100644 index 0000000..814d219 --- /dev/null +++ b/src/backend/routes/api/nginx/dead_hosts.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalDeadHost = require('../../../internal/dead-host'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/dead-hosts + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/dead-hosts + * + * Retrieve all dead-hosts + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then(data => { + return internalDeadHost.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/dead-hosts + * + * Create a new dead-host + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body) + .then(payload => { + return internalDeadHost.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific dead-host + * + * /api/nginx/dead-hosts/123 + */ +router + .route('/:host_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/dead-hosts/123 + * + * Retrieve a specific dead-host + */ + .get((req, res, next) => { + validator({ + required: ['host_id'], + additionalProperties: false, + properties: { + host_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + host_id: req.params.host_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then(data => { + return internalDeadHost.get(res.locals.access, { + id: data.host_id, + expand: data.expand + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/dead-hosts/123 + * + * Update and existing dead-host + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body) + .then(payload => { + payload.id = req.params.host_id; + return internalDeadHost.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/dead-hosts/123 + * + * Update and existing dead-host + */ + .delete((req, res, next) => { + internalDeadHost.delete(res.locals.access, {id: req.params.host_id}) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/routes/api/nginx/proxy_hosts.js b/src/backend/routes/api/nginx/proxy_hosts.js new file mode 100644 index 0000000..04cc465 --- /dev/null +++ b/src/backend/routes/api/nginx/proxy_hosts.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalProxyHost = require('../../../internal/proxy-host'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/proxy-hosts + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/proxy-hosts + * + * Retrieve all proxy-hosts + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then(data => { + return internalProxyHost.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/proxy-hosts + * + * Create a new proxy-host + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body) + .then(payload => { + return internalProxyHost.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific proxy-host + * + * /api/nginx/proxy-hosts/123 + */ +router + .route('/:host_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/proxy-hosts/123 + * + * Retrieve a specific proxy-host + */ + .get((req, res, next) => { + validator({ + required: ['host_id'], + additionalProperties: false, + properties: { + host_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + host_id: req.params.host_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then(data => { + return internalProxyHost.get(res.locals.access, { + id: data.host_id, + expand: data.expand + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/proxy-hosts/123 + * + * Update and existing proxy-host + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body) + .then(payload => { + payload.id = req.params.host_id; + return internalProxyHost.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/proxy-hosts/123 + * + * Update and existing proxy-host + */ + .delete((req, res, next) => { + internalProxyHost.delete(res.locals.access, {id: req.params.host_id}) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/routes/api/nginx/redirection_hosts.js b/src/backend/routes/api/nginx/redirection_hosts.js new file mode 100644 index 0000000..dbaf333 --- /dev/null +++ b/src/backend/routes/api/nginx/redirection_hosts.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalRedirectionHost = require('../../../internal/redirection-host'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/redirection-hosts + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/redirection-hosts + * + * Retrieve all redirection-hosts + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then(data => { + return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/redirection-hosts + * + * Create a new redirection-host + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body) + .then(payload => { + return internalRedirectionHost.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific redirection-host + * + * /api/nginx/redirection-hosts/123 + */ +router + .route('/:host_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/redirection-hosts/123 + * + * Retrieve a specific redirection-host + */ + .get((req, res, next) => { + validator({ + required: ['host_id'], + additionalProperties: false, + properties: { + host_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + host_id: req.params.host_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then(data => { + return internalRedirectionHost.get(res.locals.access, { + id: data.host_id, + expand: data.expand + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/redirection-hosts/123 + * + * Update and existing redirection-host + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body) + .then(payload => { + payload.id = req.params.host_id; + return internalRedirectionHost.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/redirection-hosts/123 + * + * Update and existing redirection-host + */ + .delete((req, res, next) => { + internalRedirectionHost.delete(res.locals.access, {id: req.params.host_id}) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/routes/api/nginx/streams.js b/src/backend/routes/api/nginx/streams.js new file mode 100644 index 0000000..8a1a061 --- /dev/null +++ b/src/backend/routes/api/nginx/streams.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalStream = require('../../../internal/stream'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/streams + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/streams + * + * Retrieve all streams + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then(data => { + return internalStream.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/streams + * + * Create a new stream + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body) + .then(payload => { + return internalStream.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific stream + * + * /api/nginx/streams/123 + */ +router + .route('/:stream_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/streams/123 + * + * Retrieve a specific stream + */ + .get((req, res, next) => { + validator({ + required: ['stream_id'], + additionalProperties: false, + properties: { + stream_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + stream_id: req.params.stream_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then(data => { + return internalStream.get(res.locals.access, { + id: data.stream_id, + expand: data.expand + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/streams/123 + * + * Update and existing stream + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body) + .then(payload => { + payload.id = req.params.stream_id; + return internalStream.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/streams/123 + * + * Update and existing stream + */ + .delete((req, res, next) => { + internalStream.delete(res.locals.access, {id: req.params.stream_id}) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/routes/api/users.js b/src/backend/routes/api/users.js index 070709c..b7ab5e6 100644 --- a/src/backend/routes/api/users.js +++ b/src/backend/routes/api/users.js @@ -3,7 +3,6 @@ const express = require('express'); const validator = require('../../lib/validator'); const jwtdecode = require('../../lib/express/jwt-decode'); -const pagination = require('../../lib/express/pagination'); const userIdFromMe = require('../../lib/express/user-id-from-me'); const internalUser = require('../../internal/user'); const apiValidator = require('../../lib/validator/api'); diff --git a/src/backend/schema/endpoints/dead-hosts.json b/src/backend/schema/endpoints/dead-hosts.json new file mode 100644 index 0000000..3775615 --- /dev/null +++ b/src/backend/schema/endpoints/dead-hosts.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/dead-hosts", + "title": "Users", + "description": "Endpoints relating to Dead Hosts", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "name": { + "description": "Name", + "example": "Jamie Curnow", + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "description": "Nickname", + "example": "Jamie", + "type": "string", + "minLength": 2, + "maxLength": 50 + }, + "email": { + "$ref": "../definitions.json#/definitions/email" + }, + "avatar": { + "description": "Avatar", + "example": "http://somewhere.jpg", + "type": "string", + "minLength": 2, + "maxLength": 150, + "readOnly": true + }, + "roles": { + "description": "Roles", + "example": [ + "admin" + ], + "type": "array" + }, + "is_disabled": { + "description": "Is Disabled", + "example": false, + "type": "boolean" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Users", + "href": "/users", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new User", + "href": "/users", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "required": [ + "name", + "nickname", + "email" + ], + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ], + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "avatar": { + "$ref": "#/definitions/avatar" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } +} diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json new file mode 100644 index 0000000..02f4496 --- /dev/null +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/proxy-hosts", + "title": "Users", + "description": "Endpoints relating to Proxy Hosts", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "name": { + "description": "Name", + "example": "Jamie Curnow", + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "description": "Nickname", + "example": "Jamie", + "type": "string", + "minLength": 2, + "maxLength": 50 + }, + "email": { + "$ref": "../definitions.json#/definitions/email" + }, + "avatar": { + "description": "Avatar", + "example": "http://somewhere.jpg", + "type": "string", + "minLength": 2, + "maxLength": 150, + "readOnly": true + }, + "roles": { + "description": "Roles", + "example": [ + "admin" + ], + "type": "array" + }, + "is_disabled": { + "description": "Is Disabled", + "example": false, + "type": "boolean" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Users", + "href": "/users", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new User", + "href": "/users", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "required": [ + "name", + "nickname", + "email" + ], + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ], + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "avatar": { + "$ref": "#/definitions/avatar" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } +} diff --git a/src/backend/schema/endpoints/redirection-hosts.json b/src/backend/schema/endpoints/redirection-hosts.json new file mode 100644 index 0000000..706d2ee --- /dev/null +++ b/src/backend/schema/endpoints/redirection-hosts.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/redirection-hosts", + "title": "Users", + "description": "Endpoints relating to Redirection Hosts", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "name": { + "description": "Name", + "example": "Jamie Curnow", + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "description": "Nickname", + "example": "Jamie", + "type": "string", + "minLength": 2, + "maxLength": 50 + }, + "email": { + "$ref": "../definitions.json#/definitions/email" + }, + "avatar": { + "description": "Avatar", + "example": "http://somewhere.jpg", + "type": "string", + "minLength": 2, + "maxLength": 150, + "readOnly": true + }, + "roles": { + "description": "Roles", + "example": [ + "admin" + ], + "type": "array" + }, + "is_disabled": { + "description": "Is Disabled", + "example": false, + "type": "boolean" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Users", + "href": "/users", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new User", + "href": "/users", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "required": [ + "name", + "nickname", + "email" + ], + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ], + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "avatar": { + "$ref": "#/definitions/avatar" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } +} diff --git a/src/backend/schema/endpoints/streams.json b/src/backend/schema/endpoints/streams.json new file mode 100644 index 0000000..03ed840 --- /dev/null +++ b/src/backend/schema/endpoints/streams.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/streams", + "title": "Users", + "description": "Endpoints relating to Streams", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "name": { + "description": "Name", + "example": "Jamie Curnow", + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "description": "Nickname", + "example": "Jamie", + "type": "string", + "minLength": 2, + "maxLength": 50 + }, + "email": { + "$ref": "../definitions.json#/definitions/email" + }, + "avatar": { + "description": "Avatar", + "example": "http://somewhere.jpg", + "type": "string", + "minLength": 2, + "maxLength": 150, + "readOnly": true + }, + "roles": { + "description": "Roles", + "example": [ + "admin" + ], + "type": "array" + }, + "is_disabled": { + "description": "Is Disabled", + "example": false, + "type": "boolean" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Users", + "href": "/users", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new User", + "href": "/users", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "required": [ + "name", + "nickname", + "email" + ], + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing User", + "href": "/users/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ], + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "name": { + "$ref": "#/definitions/name" + }, + "nickname": { + "$ref": "#/definitions/nickname" + }, + "email": { + "$ref": "#/definitions/email" + }, + "avatar": { + "$ref": "#/definitions/avatar" + }, + "roles": { + "$ref": "#/definitions/roles" + }, + "is_disabled": { + "$ref": "#/definitions/is_disabled" + } + } +} diff --git a/src/backend/schema/index.json b/src/backend/schema/index.json index 102d055..6fc442e 100644 --- a/src/backend/schema/index.json +++ b/src/backend/schema/index.json @@ -16,6 +16,18 @@ }, "users": { "$ref": "endpoints/users.json" + }, + "proxy-hosts": { + "$ref": "endpoints/proxy-hosts.json" + }, + "redirection-hosts": { + "$ref": "endpoints/redirection-hosts.json" + }, + "dead-hosts": { + "$ref": "endpoints/dead-hosts.json" + }, + "streams": { + "$ref": "endpoints/streams.json" } } } diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index b689136..d6cb348 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -103,6 +103,26 @@ function makeExpansionString (expand) { return items.join(','); } +/** + * @param {String} path + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ +function getAllObjects (path, expand, query) { + let params = []; + + if (typeof expand === 'object' && expand !== null && expand.length) { + params.push('expand=' + makeExpansionString(expand)); + } + + if (typeof query === 'string') { + params.push('query=' + query); + } + + return fetch('get', path + (params.length ? '?' + params.join('&') : '')); +} + module.exports = { status: function () { return fetch('get', ''); @@ -168,17 +188,7 @@ module.exports = { * @returns {Promise} */ getAll: function (expand, query) { - let params = []; - - if (typeof expand === 'object' && expand !== null && expand.length) { - params.push('expand=' + makeExpansionString(expand)); - } - - if (typeof query === 'string') { - params.push('query=' + query); - } - - return fetch('get', 'users' + (params.length ? '?' + params.join('&') : '')); + return getAllObjects('users', expand, query); }, /** @@ -237,6 +247,64 @@ module.exports = { } }, + Nginx: { + + ProxyHosts: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/proxy-hosts', expand, query); + } + }, + + RedirectionHosts: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/redirection-hosts', expand, query); + } + }, + + Streams: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/streams', expand, query); + } + }, + + DeadHosts: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/dead-hosts', expand, query); + } + } + }, + + AccessLists: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('access-lists', expand, query); + } + }, + Reports: { /** @@ -244,6 +312,6 @@ module.exports = { */ getHostStats: function () { return fetch('get', 'reports/hosts'); - }, + } } }; diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index d7ea292..0a78388 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -124,60 +124,70 @@ module.exports = { * Nginx Proxy Hosts */ showNginxProxy: function () { - let controller = this; + if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) { + let controller = this; - require(['./main', './nginx/proxy/main'], (App, View) => { - controller.navigate('/nginx/proxy'); - App.UI.showAppContent(new View()); - }); + require(['./main', './nginx/proxy/main'], (App, View) => { + controller.navigate('/nginx/proxy'); + App.UI.showAppContent(new View()); + }); + } }, /** * Nginx Redirection Hosts */ showNginxRedirection: function () { - let controller = this; + if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { + let controller = this; - require(['./main', './nginx/redirection/main'], (App, View) => { - controller.navigate('/nginx/redirection'); - App.UI.showAppContent(new View()); - }); + require(['./main', './nginx/redirection/main'], (App, View) => { + controller.navigate('/nginx/redirection'); + App.UI.showAppContent(new View()); + }); + } }, /** * Nginx Stream Hosts */ showNginxStream: function () { - let controller = this; + if (Cache.User.isAdmin() || Cache.User.canView('streams')) { + let controller = this; - require(['./main', './nginx/stream/main'], (App, View) => { - controller.navigate('/nginx/stream'); - App.UI.showAppContent(new View()); - }); + require(['./main', './nginx/stream/main'], (App, View) => { + controller.navigate('/nginx/stream'); + App.UI.showAppContent(new View()); + }); + } }, /** - * Nginx 404 Hosts + * Nginx Dead Hosts */ - showNginx404: function () { - let controller = this; + showNginxDead: function () { + if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { + let controller = this; - require(['./main', './nginx/404/main'], (App, View) => { - controller.navigate('/nginx/404'); - App.UI.showAppContent(new View()); - }); + require(['./main', './nginx/dead/main'], (App, View) => { + controller.navigate('/nginx/404'); + App.UI.showAppContent(new View()); + }); + } }, /** * Nginx Access */ showNginxAccess: function () { - let controller = this; + if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { + let controller = this; - require(['./main', './nginx/access/main'], (App, View) => { - controller.navigate('/nginx/access'); - App.UI.showAppContent(new View()); - }); + require(['./main', './nginx/access/main'], (App, View) => { + controller.navigate('/nginx/access'); + App.UI.showAppContent(new View()); + }); + } }, /** diff --git a/src/frontend/js/app/dashboard/main.ejs b/src/frontend/js/app/dashboard/main.ejs index e752b88..61bf7f5 100644 --- a/src/frontend/js/app/dashboard/main.ejs +++ b/src/frontend/js/app/dashboard/main.ejs @@ -2,8 +2,10 @@
<%- subtitle %>
+<% } + +if (link) { %> + <%- link %> +<% } %> diff --git a/src/frontend/js/app/empty/main.js b/src/frontend/js/app/empty/main.js new file mode 100644 index 0000000..0ec2466 --- /dev/null +++ b/src/frontend/js/app/empty/main.js @@ -0,0 +1,30 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./main.ejs'); + +module.exports = Mn.View.extend({ + className: 'text-center m-7', + template: template, + + ui: { + action: 'a' + }, + + events: { + 'click @ui.action': function (e) { + e.preventDefault(); + this.getOption('action')(); + } + }, + + templateContext: function () { + return { + title: this.getOption('title'), + subtitle: this.getOption('subtitle'), + link: this.getOption('link'), + action: typeof this.getOption('action') === 'function' + }; + } + +}); diff --git a/src/frontend/js/app/error/main.ejs b/src/frontend/js/app/error/main.ejs new file mode 100644 index 0000000..13e940f --- /dev/null +++ b/src/frontend/js/app/error/main.ejs @@ -0,0 +1,7 @@ + +<%= code ? '' + code + ' — ' : '' %> +<%- message %> + +<% if (retry) { %> +