diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index 6efaf5d..cadbc73 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const deadHostModel = require('../models/dead_host'); +const internalHost = require('./host'); function omissions () { return ['is_deleted']; @@ -10,6 +11,199 @@ function omissions () { const internalDeadHost = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('dead_hosts:create', data) + .then(access_data => { + // Get a list of the domain names and check each of them against existing records + let domain_name_check_promises = []; + + data.domain_names.map(function (domain_name) { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); + }); + + return Promise.all(domain_name_check_promises) + .then(check_results => { + check_results.map(function (result) { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } + }); + }); + }) + .then(() => { + // At this point the domains should have been checked + data.owner_user_id = access.token.get('attrs').id; + + if (typeof data.meta === 'undefined') { + data.meta = {}; + } + + return deadHostModel + .query() + .omit(omissions()) + .insertAndFetch(data); + }) + .then(row => { + return _.omit(row, omissions()); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.email] + * @param {String} [data.name] + * @return {Promise} + */ + update: (access, data) => { + return access.can('dead_hosts:update', data.id) + .then(access_data => { + // Get a list of the domain names and check each of them against existing records + let domain_name_check_promises = []; + + if (typeof data.domain_names !== 'undefined') { + data.domain_names.map(function (domain_name) { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id)); + }); + + return Promise.all(domain_name_check_promises) + .then(check_results => { + check_results.map(function (result) { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } + }); + }); + } + }) + .then(() => { + return internalDeadHost.get(access, {id: data.id}); + }) + .then(row => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return deadHostModel + .query() + .omit(omissions()) + .patchAndFetchById(row.id, data) + .then(saved_row => { + saved_row.meta = internalHost.cleanMeta(saved_row.meta); + return _.omit(saved_row, omissions()); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + if (typeof data === 'undefined') { + data = {}; + } + + return access.can('dead_hosts:get', data.id) + .then(access_data => { + let query = deadHostModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowEager('[owner]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // 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) { + row.meta = internalHost.cleanMeta(row.meta); + 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('dead_hosts:delete', data.id) + .then(() => { + return internalDeadHost.get(access, {id: data.id}); + }) + .then(row => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return deadHostModel + .query() + .where('id', row.id) + .patch({ + is_deleted: 1 + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {Object} data.files + * @returns {Promise} + */ + setCerts: (access, data) => { + return internalDeadHost.get(access, {id: data.id}) + .then(row => { + _.map(data.files, (file, name) => { + if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { + row.meta[name] = file.data.toString(); + } + }); + + return internalDeadHost.update(access, { + id: data.id, + meta: row.meta + }); + }) + .then(row => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); + }, + /** * All Hosts * @@ -26,6 +220,7 @@ const internalDeadHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) + .allowEager('[owner]') .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { @@ -44,6 +239,13 @@ const internalDeadHost = { } return query; + }) + .then(rows => { + rows.map(row => { + row.meta = internalHost.cleanMeta(row.meta); + }); + + return rows; }); }, diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 649a3df..ded58d9 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -115,10 +115,6 @@ const internalProxyHost = { data = {}; } - if (typeof data.id === 'undefined' || !data.id) { - data.id = access.token.get('attrs').id; - } - return access.can('proxy_hosts:get', data.id) .then(access_data => { let query = proxyHostModel diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index 75e5447..7f0d711 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -115,10 +115,6 @@ const internalRedirectionHost = { data = {}; } - if (typeof data.id === 'undefined' || !data.id) { - data.id = access.token.get('attrs').id; - } - return access.can('redirection_hosts:get', data.id) .then(access_data => { let query = redirectionHostModel diff --git a/src/backend/internal/stream.js b/src/backend/internal/stream.js index 7e30ac4..603b8fd 100644 --- a/src/backend/internal/stream.js +++ b/src/backend/internal/stream.js @@ -11,7 +11,137 @@ function omissions () { const internalStream = { /** - * All Hosts + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('streams:create', data) + .then(access_data => { + // TODO: At this point the existing ports should have been checked + data.owner_user_id = access.token.get('attrs').id; + + if (typeof data.meta === 'undefined') { + data.meta = {}; + } + + return streamModel + .query() + .omit(omissions()) + .insertAndFetch(data); + }) + .then(row => { + return _.omit(row, omissions()); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.email] + * @param {String} [data.name] + * @return {Promise} + */ + update: (access, data) => { + return access.can('streams:update', data.id) + .then(access_data => { + // TODO: at this point the existing streams should have been checked + return internalStream.get(access, {id: data.id}); + }) + .then(row => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return streamModel + .query() + .omit(omissions()) + .patchAndFetchById(row.id, data) + .then(saved_row => { + return _.omit(saved_row, omissions()); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + if (typeof data === 'undefined') { + data = {}; + } + + return access.can('streams:get', data.id) + .then(access_data => { + let query = streamModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowEager('[owner]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // 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('streams:delete', data.id) + .then(() => { + return internalStream.get(access, {id: data.id}); + }) + .then(row => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return streamModel + .query() + .where('id', row.id) + .patch({ + is_deleted: 1 + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Streams * * @param {Access} access * @param {Array} [expand] @@ -26,6 +156,7 @@ const internalStream = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) + .allowEager('[owner]') .orderBy('incoming_port', 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/src/backend/lib/access/dead_hosts-create.json b/src/backend/lib/access/dead_hosts-create.json new file mode 100644 index 0000000..12fc4af --- /dev/null +++ b/src/backend/lib/access/dead_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/dead_hosts-delete.json b/src/backend/lib/access/dead_hosts-delete.json new file mode 100644 index 0000000..12fc4af --- /dev/null +++ b/src/backend/lib/access/dead_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/dead_hosts-get.json b/src/backend/lib/access/dead_hosts-get.json new file mode 100644 index 0000000..925b52c --- /dev/null +++ b/src/backend/lib/access/dead_hosts-get.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/dead_hosts-update.json b/src/backend/lib/access/dead_hosts-update.json new file mode 100644 index 0000000..12fc4af --- /dev/null +++ b/src/backend/lib/access/dead_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/streams-create.json b/src/backend/lib/access/streams-create.json new file mode 100644 index 0000000..6a745ec --- /dev/null +++ b/src/backend/lib/access/streams-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/streams-delete.json b/src/backend/lib/access/streams-delete.json new file mode 100644 index 0000000..6a745ec --- /dev/null +++ b/src/backend/lib/access/streams-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/streams-get.json b/src/backend/lib/access/streams-get.json new file mode 100644 index 0000000..3443aa8 --- /dev/null +++ b/src/backend/lib/access/streams-get.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/lib/access/streams-update.json b/src/backend/lib/access/streams-update.json new file mode 100644 index 0000000..6a745ec --- /dev/null +++ b/src/backend/lib/access/streams-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 893e648..3ce8d01 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -93,6 +93,7 @@ exports.up = function (knex/*, Promise*/) { table.integer('preserve_path').notNull().unsigned().defaultTo(0); table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); table.string('ssl_provider').notNull().defaultTo(''); + table.integer('ssl_forced').notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0); table.json('meta').notNull(); }); @@ -109,6 +110,7 @@ exports.up = function (knex/*, Promise*/) { table.json('domain_names').notNull(); table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); table.string('ssl_provider').notNull().defaultTo(''); + table.integer('ssl_forced').notNull().unsigned().defaultTo(0); table.json('meta').notNull(); }); }) diff --git a/src/backend/routes/api/nginx/dead_hosts.js b/src/backend/routes/api/nginx/dead_hosts.js index 814d219..c65dc2c 100644 --- a/src/backend/routes/api/nginx/dead_hosts.js +++ b/src/backend/routes/api/nginx/dead_hosts.js @@ -104,7 +104,7 @@ router }) .then(data => { return internalDeadHost.get(res.locals.access, { - id: data.host_id, + id: parseInt(data.host_id, 10), expand: data.expand }); }) @@ -123,7 +123,7 @@ router .put((req, res, next) => { apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body) .then(payload => { - payload.id = req.params.host_id; + payload.id = parseInt(req.params.host_id, 10); return internalDeadHost.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing dead-host */ .delete((req, res, next) => { - internalDeadHost.delete(res.locals.access, {id: req.params.host_id}) + internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) .then(result => { res.status(200) .send(result); @@ -147,4 +147,38 @@ router .catch(next); }); +/** + * Specific dead-host Certificates + * + * /api/nginx/dead-hosts/123/certificates + */ +router + .route('/:host_id/certificates') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * POST /api/nginx/dead-hosts/123/certificates + * + * Upload certifications + */ + .post((req, res, next) => { + if (!req.files) { + res.status(400) + .send({error: 'No files were uploaded'}); + } else { + internalDeadHost.setCerts(res.locals.access, { + id: parseInt(req.params.host_id, 10), + files: req.files + }) + .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 index 8a1a061..80f4c9f 100644 --- a/src/backend/routes/api/nginx/streams.js +++ b/src/backend/routes/api/nginx/streams.js @@ -94,17 +94,17 @@ router stream_id: { $ref: 'definitions#/definitions/id' }, - expand: { + expand: { $ref: 'definitions#/definitions/expand' } } }, { stream_id: req.params.stream_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) .then(data => { return internalStream.get(res.locals.access, { - id: data.stream_id, + id: parseInt(data.stream_id, 10), expand: data.expand }); }) @@ -123,7 +123,7 @@ router .put((req, res, next) => { apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body) .then(payload => { - payload.id = req.params.stream_id; + payload.id = parseInt(req.params.stream_id, 10); return internalStream.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing stream */ .delete((req, res, next) => { - internalStream.delete(res.locals.access, {id: req.params.stream_id}) + internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)}) .then(result => { res.status(200) .send(result); diff --git a/src/backend/schema/endpoints/dead-hosts.json b/src/backend/schema/endpoints/dead-hosts.json index 3775615..6fbe130 100644 --- a/src/backend/schema/endpoints/dead-hosts.json +++ b/src/backend/schema/endpoints/dead-hosts.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "endpoints/dead-hosts", - "title": "Users", - "description": "Endpoints relating to Dead Hosts", + "title": "404 Hosts", + "description": "Endpoints relating to 404 Hosts", "stability": "stable", "type": "object", "definitions": { @@ -15,49 +15,63 @@ "modified_on": { "$ref": "../definitions.json#/definitions/modified_on" }, - "name": { - "description": "Name", - "example": "Jamie Curnow", - "type": "string", - "minLength": 2, - "maxLength": 100 + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" }, - "nickname": { - "description": "Nickname", - "example": "Jamie", - "type": "string", - "minLength": 2, - "maxLength": 50 + "ssl_enabled": { + "$ref": "../definitions.json#/definitions/ssl_enabled" }, - "email": { - "$ref": "../definitions.json#/definitions/email" + "ssl_forced": { + "$ref": "../definitions.json#/definitions/ssl_forced" }, - "avatar": { - "description": "Avatar", - "example": "http://somewhere.jpg", - "type": "string", - "minLength": 2, - "maxLength": 150, - "readOnly": true + "ssl_provider": { + "$ref": "../definitions.json#/definitions/ssl_provider" }, - "roles": { - "description": "Roles", - "example": [ - "admin" - ], - "type": "array" + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "letsencrypt_email": { + "type": "string", + "format": "email" + }, + "letsencrypt_agree": { + "type": "boolean" + } + } + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" }, - "is_disabled": { - "description": "Is Disabled", - "example": false, - "type": "boolean" + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "domain_names": { + "$ref": "#/definitions/domain_names" + }, + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" + }, + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "meta": { + "$ref": "#/definitions/meta" } }, "links": [ { "title": "List", - "description": "Returns a list of Users", - "href": "/users", + "description": "Returns a list of 404 Hosts", + "href": "/nginx/dead-hosts", "access": "private", "method": "GET", "rel": "self", @@ -73,8 +87,8 @@ }, { "title": "Create", - "description": "Creates a new User", - "href": "/users", + "description": "Creates a new 404 Host", + "href": "/nginx/dead-hosts", "access": "private", "method": "POST", "rel": "create", @@ -83,34 +97,25 @@ }, "schema": { "type": "object", + "additionalProperties": false, "required": [ - "name", - "nickname", - "email" + "domain_names" ], "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "email": { - "$ref": "#/definitions/email" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -122,8 +127,8 @@ }, { "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Updates a existing 404 Host", + "href": "/nginx/dead-hosts/{definitions.identity.example}", "access": "private", "method": "PUT", "rel": "update", @@ -132,21 +137,22 @@ }, "schema": { "type": "object", + "additionalProperties": false, "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "email": { - "$ref": "#/definitions/email" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -158,8 +164,8 @@ }, { "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Deletes a existing 404 Host", + "href": "/nginx/dead-hosts/{definitions.identity.example}", "access": "private", "method": "DELETE", "rel": "delete", @@ -170,34 +176,5 @@ "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 index 2e0a9d4..3f45960 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -130,6 +130,7 @@ }, "schema": { "type": "object", + "additionalProperties": false, "required": [ "domain_names", "forward_ip", @@ -186,6 +187,7 @@ }, "schema": { "type": "object", + "additionalProperties": false, "properties": { "domain_names": { "$ref": "#/definitions/domain_names" diff --git a/src/backend/schema/endpoints/redirection-hosts.json b/src/backend/schema/endpoints/redirection-hosts.json index a66b50b..4dbe0da 100644 --- a/src/backend/schema/endpoints/redirection-hosts.json +++ b/src/backend/schema/endpoints/redirection-hosts.json @@ -117,6 +117,7 @@ }, "schema": { "type": "object", + "additionalProperties": false, "required": [ "domain_names", "forward_domain_name" @@ -166,6 +167,7 @@ }, "schema": { "type": "object", + "additionalProperties": false, "properties": { "domain_names": { "$ref": "#/definitions/domain_names" diff --git a/src/backend/schema/endpoints/streams.json b/src/backend/schema/endpoints/streams.json index 03ed840..e8535a3 100644 --- a/src/backend/schema/endpoints/streams.json +++ b/src/backend/schema/endpoints/streams.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "endpoints/streams", - "title": "Users", + "title": "Streams", "description": "Endpoints relating to Streams", "stability": "stable", "type": "object", @@ -15,49 +15,64 @@ "modified_on": { "$ref": "../definitions.json#/definitions/modified_on" }, - "name": { - "description": "Name", - "example": "Jamie Curnow", + "incoming_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "forward_ip": { "type": "string", - "minLength": 2, - "maxLength": 100 + "format": "ipv4" }, - "nickname": { - "description": "Nickname", - "example": "Jamie", - "type": "string", - "minLength": 2, - "maxLength": 50 + "forwarding_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 }, - "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, + "tcp_forwarding": { "type": "boolean" + }, + "udp_forwarding": { + "type": "boolean" + }, + "meta": { + "type": "object" + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "incoming_port": { + "$ref": "#/definitions/incoming_port" + }, + "forward_ip": { + "$ref": "#/definitions/forward_ip" + }, + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" + }, + "tcp_forwarding": { + "$ref": "#/definitions/tcp_forwarding" + }, + "udp_forwarding": { + "$ref": "#/definitions/udp_forwarding" + }, + "meta": { + "$ref": "#/definitions/meta" } }, "links": [ { "title": "List", - "description": "Returns a list of Users", - "href": "/users", + "description": "Returns a list of Steams", + "href": "/nginx/streams", "access": "private", "method": "GET", "rel": "self", @@ -73,8 +88,8 @@ }, { "title": "Create", - "description": "Creates a new User", - "href": "/users", + "description": "Creates a new Stream", + "href": "/nginx/streams", "access": "private", "method": "POST", "rel": "create", @@ -83,34 +98,30 @@ }, "schema": { "type": "object", + "additionalProperties": false, "required": [ - "name", - "nickname", - "email" + "incoming_port", + "forward_ip", + "forwarding_port" ], "properties": { - "name": { - "$ref": "#/definitions/name" + "incoming_port": { + "$ref": "#/definitions/incoming_port" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_ip": { + "$ref": "#/definitions/forward_ip" }, - "email": { - "$ref": "#/definitions/email" + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" }, - "roles": { - "$ref": "#/definitions/roles" + "tcp_forwarding": { + "$ref": "#/definitions/tcp_forwarding" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "udp_forwarding": { + "$ref": "#/definitions/udp_forwarding" }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -122,8 +133,8 @@ }, { "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Updates a existing Stream", + "href": "/nginx/streams/{definitions.identity.example}", "access": "private", "method": "PUT", "rel": "update", @@ -132,21 +143,25 @@ }, "schema": { "type": "object", + "additionalProperties": false, "properties": { - "name": { - "$ref": "#/definitions/name" + "incoming_port": { + "$ref": "#/definitions/incoming_port" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_ip": { + "$ref": "#/definitions/forward_ip" }, - "email": { - "$ref": "#/definitions/email" + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" }, - "roles": { - "$ref": "#/definitions/roles" + "tcp_forwarding": { + "$ref": "#/definitions/tcp_forwarding" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "udp_forwarding": { + "$ref": "#/definitions/udp_forwarding" + }, + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -158,8 +173,8 @@ }, { "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Deletes a existing Stream", + "href": "/nginx/streams/{definitions.identity.example}", "access": "private", "method": "DELETE", "rel": "delete", @@ -170,34 +185,5 @@ "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/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 46ff67c..7e80684 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -215,18 +215,31 @@ module.exports = { }, /** - * Nginx Stream Form + * Stream Form * * @param [model] */ showNginxStreamForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { + if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { require(['./main', './nginx/stream/form'], function (App, View) { App.UI.showModalDialog(new View({model: model})); }); } }, + /** + * Stream Delete Confirm + * + * @param model + */ + showNginxStreamDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { + require(['./main', './nginx/stream/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Nginx Dead Hosts */ @@ -241,6 +254,32 @@ module.exports = { } }, + /** + * Dead Host Form + * + * @param [model] + */ + showNginxDeadForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { + require(['./main', './nginx/dead/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + + /** + * Dead Host Delete Confirm + * + * @param model + */ + showNginxDeadDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { + require(['./main', './nginx/dead/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Nginx Access */ diff --git a/src/frontend/js/app/nginx/dead/delete.ejs b/src/frontend/js/app/nginx/dead/delete.ejs new file mode 100644 index 0000000..27e4434 --- /dev/null +++ b/src/frontend/js/app/nginx/dead/delete.ejs @@ -0,0 +1,23 @@ +
diff --git a/src/frontend/js/app/nginx/dead/delete.js b/src/frontend/js/app/nginx/dead/delete.js new file mode 100644 index 0000000..0ae2486 --- /dev/null +++ b/src/frontend/js/app/nginx/dead/delete.js @@ -0,0 +1,36 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./delete.ejs'); + +require('jquery-serializejson'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + App.Api.Nginx.DeadHosts.delete(this.model.get('id')) + .then(() => { + App.Controller.showNginxDead(); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + } +}); diff --git a/src/frontend/js/app/nginx/dead/form.ejs b/src/frontend/js/app/nginx/dead/form.ejs new file mode 100644 index 0000000..6c9698f --- /dev/null +++ b/src/frontend/js/app/nginx/dead/form.ejs @@ -0,0 +1,110 @@ + diff --git a/src/frontend/js/app/nginx/dead/form.js b/src/frontend/js/app/nginx/dead/form.js new file mode 100644 index 0000000..a53875e --- /dev/null +++ b/src/frontend/js/app/nginx/dead/form.js @@ -0,0 +1,181 @@ +'use strict'; + +const _ = require('underscore'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const DeadHostModel = require('../../../models/dead-host'); +const template = require('./form.ejs'); + +require('jquery-serializejson'); +require('selectize'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + max_file_size: 5120, + + ui: { + form: 'form', + domain_names: 'input[name="domain_names"]', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save', + ssl_enabled: 'input[name="ssl_enabled"]', + ssl_options: '#ssl-options input', + ssl_provider: 'input[name="ssl_provider"]', + other_ssl_certificate: '#other_ssl_certificate', + other_ssl_certificate_key: '#other_ssl_certificate_key', + + // SSL hiding and showing + all_ssl: '.letsencrypt-ssl, .other-ssl', + letsencrypt_ssl: '.letsencrypt-ssl', + other_ssl: '.other-ssl' + }, + + events: { + 'change @ui.ssl_enabled': function () { + let enabled = this.ui.ssl_enabled.prop('checked'); + this.ui.ssl_options.not(this.ui.ssl_enabled).prop('disabled', !enabled).parents('.form-group').css('opacity', enabled ? 1 : 0.5); + this.ui.ssl_provider.trigger('change'); + }, + + 'change @ui.ssl_provider': function () { + let enabled = this.ui.ssl_enabled.prop('checked'); + let provider = this.ui.ssl_provider.filter(':checked').val(); + this.ui.all_ssl.hide().find('input').prop('disabled', true); + this.ui[provider + '_ssl'].show().find('input').prop('disabled', !enabled); + }, + + 'click @ui.save': function (e) { + e.preventDefault(); + + if (!this.ui.form[0].checkValidity()) { + $('').hide().appendTo(this.ui.form).click().remove(); + return; + } + + let view = this; + let data = this.ui.form.serializeJSON(); + + // Manipulate + data.ssl_enabled = !!data.ssl_enabled; + data.ssl_forced = !!data.ssl_forced; + + if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { + data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; + } + + if (typeof data.domain_names === 'string' && data.domain_names) { + data.domain_names = data.domain_names.split(','); + } + + let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other'; + let ssl_files = []; + let method = App.Api.Nginx.DeadHosts.create; + let is_new = true; + + let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other'); + + if (this.model.get('id')) { + // edit + is_new = false; + method = App.Api.Nginx.DeadHosts.update; + data.id = this.model.get('id'); + } + + // check files are attached + if (require_ssl_files) { + if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) { + if (must_require_ssl_files) { + alert('certificate file is not attached'); + return; + } + } else { + if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) { + alert('certificate file is too large (> 5kb)'); + return; + } + ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]}); + } + + if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) { + if (must_require_ssl_files) { + alert('certificate key file is not attached'); + return; + } + } else { + if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) { + alert('certificate key file is too large (> 5kb)'); + return; + } + ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]}); + } + } + + this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + method(data) + .then(result => { + view.model.set(result); + + // Now upload the certs if we need to + if (ssl_files.length) { + let form_data = new FormData(); + + ssl_files.map(function (file) { + form_data.append(file.name, file.file); + }); + + return App.Api.Nginx.DeadHosts.setCerts(view.model.get('id'), form_data) + .then(result => { + view.model.set('meta', _.assign({}, view.model.get('meta'), result)); + }); + } + }) + .then(() => { + App.UI.closeModal(function () { + if (is_new) { + App.Controller.showNginxDead(); + } + }); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + }, + + templateContext: { + getLetsencryptEmail: function () { + return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); + }, + + getLetsencryptAgree: function () { + return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; + } + }, + + onRender: function () { + this.ui.ssl_enabled.trigger('change'); + this.ui.ssl_provider.trigger('change'); + + this.ui.domain_names.selectize({ + delimiter: ',', + persist: false, + maxOptions: 15, + create: function (input) { + return { + value: input, + text: input + }; + }, + createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ + }); + }, + + initialize: function (options) { + if (typeof options.model === 'undefined' || !options.model) { + this.model = new DeadHostModel.Model(); + } + } +}); diff --git a/src/frontend/js/app/nginx/dead/list/item.ejs b/src/frontend/js/app/nginx/dead/list/item.ejs index bd4d19e..71b3cda 100644 --- a/src/frontend/js/app/nginx/dead/list/item.ejs +++ b/src/frontend/js/app/nginx/dead/list/item.ejs @@ -1,32 +1,34 @@