From 5107d1529fb5b8a66efb182521cf39ac0cddfb70 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 26 Jul 2018 08:23:32 +1000 Subject: [PATCH] 404 hosts section and other fixes --- src/backend/internal/dead-host.js | 202 ++++++++++++++++++ src/backend/internal/proxy-host.js | 4 - src/backend/internal/redirection-host.js | 4 - src/backend/internal/stream.js | 133 +++++++++++- src/backend/lib/access/dead_hosts-create.json | 23 ++ src/backend/lib/access/dead_hosts-delete.json | 23 ++ src/backend/lib/access/dead_hosts-get.json | 23 ++ src/backend/lib/access/dead_hosts-update.json | 23 ++ src/backend/lib/access/streams-create.json | 23 ++ src/backend/lib/access/streams-delete.json | 23 ++ src/backend/lib/access/streams-get.json | 23 ++ src/backend/lib/access/streams-update.json | 23 ++ .../migrations/20180618015850_initial.js | 2 + src/backend/routes/api/nginx/dead_hosts.js | 40 +++- src/backend/routes/api/nginx/streams.js | 10 +- src/backend/schema/endpoints/dead-hosts.json | 181 +++++++--------- src/backend/schema/endpoints/proxy-hosts.json | 2 + .../schema/endpoints/redirection-hosts.json | 2 + src/backend/schema/endpoints/streams.json | 188 ++++++++-------- src/frontend/js/app/controller.js | 43 +++- src/frontend/js/app/nginx/dead/delete.ejs | 23 ++ src/frontend/js/app/nginx/dead/delete.js | 36 ++++ src/frontend/js/app/nginx/dead/form.ejs | 110 ++++++++++ src/frontend/js/app/nginx/dead/form.js | 181 ++++++++++++++++ src/frontend/js/app/nginx/dead/list/item.ejs | 32 +-- src/frontend/js/app/nginx/dead/list/item.js | 53 +---- src/frontend/js/app/nginx/dead/list/main.ejs | 7 +- src/frontend/js/app/nginx/dead/list/main.js | 11 +- src/frontend/js/app/nginx/dead/main.ejs | 4 +- src/frontend/js/app/nginx/dead/main.js | 29 ++- src/frontend/js/app/nginx/proxy/form.js | 26 +-- src/frontend/js/app/nginx/redirection/form.js | 20 +- src/frontend/js/app/nginx/stream/form.ejs | 9 +- src/frontend/js/app/nginx/stream/form.js | 33 +-- .../js/app/nginx/stream/list/item.ejs | 24 +-- src/frontend/js/app/nginx/stream/list/item.js | 6 +- .../js/app/nginx/stream/list/main.ejs | 5 +- src/frontend/js/app/nginx/stream/list/main.js | 2 +- src/frontend/js/i18n/messages.json | 19 +- src/frontend/js/models/dead-host.js | 8 +- src/frontend/js/models/stream.js | 6 +- 41 files changed, 1260 insertions(+), 379 deletions(-) create mode 100644 src/backend/lib/access/dead_hosts-create.json create mode 100644 src/backend/lib/access/dead_hosts-delete.json create mode 100644 src/backend/lib/access/dead_hosts-get.json create mode 100644 src/backend/lib/access/dead_hosts-update.json create mode 100644 src/backend/lib/access/streams-create.json create mode 100644 src/backend/lib/access/streams-delete.json create mode 100644 src/backend/lib/access/streams-get.json create mode 100644 src/backend/lib/access/streams-update.json create mode 100644 src/frontend/js/app/nginx/dead/delete.ejs create mode 100644 src/frontend/js/app/nginx/dead/delete.js create mode 100644 src/frontend/js/app/nginx/dead/form.ejs create mode 100644 src/frontend/js/app/nginx/dead/form.js diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index 6efaf5d8..cadbc73f 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 649a3dfe..ded58d94 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 75e5447b..7f0d711f 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 7e30ac4a..603b8fd7 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 00000000..12fc4af0 --- /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 00000000..12fc4af0 --- /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 00000000..925b52ce --- /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 00000000..12fc4af0 --- /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 00000000..6a745ec4 --- /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 00000000..6a745ec4 --- /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 00000000..3443aa85 --- /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 00000000..6a745ec4 --- /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 893e6480..3ce8d018 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 814d2192..c65dc2c8 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 8a1a0613..80f4c9fd 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 3775615b..6fbe130f 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 2e0a9d43..3f459609 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 a66b50be..4dbe0dab 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 03ed840a..e8535a31 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 46ff67cc..7e806840 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 00000000..27e44341 --- /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 00000000..0ae2486e --- /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 00000000..6c9698fa --- /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 00000000..a53875ec --- /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 bd4d19e0..71b3cda3 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 @@ -
- +
+
-
<%- name %>
+
+ <% domain_names.map(function(host) { + %> + <%- host %> + <% + }); + %> +
- Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %> + <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
-
<%- email %>
- - -
<%- roles.join(', ') %>
+
<%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %>
+<% if (canManage) { %> +<% } %> \ No newline at end of file diff --git a/src/frontend/js/app/nginx/dead/list/item.js b/src/frontend/js/app/nginx/dead/list/item.js index e2a68255..fcde973c 100644 --- a/src/frontend/js/app/nginx/dead/list/item.js +++ b/src/frontend/js/app/nginx/dead/list/item.js @@ -1,69 +1,32 @@ 'use strict'; -const Mn = require('backbone.marionette'); -const Controller = require('../../../controller'); -const Api = require('../../../api'); -const Cache = require('../../../cache'); -const Tokens = require('../../../tokens'); -const template = require('./item.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../../main'); +const template = require('./item.ejs'); module.exports = Mn.View.extend({ template: template, tagName: 'tr', ui: { - edit: 'a.edit-user', - permissions: 'a.edit-permissions', - password: 'a.set-password', - login: 'a.login', - delete: 'a.delete-user' + edit: 'a.edit', + delete: 'a.delete' }, events: { 'click @ui.edit': function (e) { e.preventDefault(); - Controller.showUserForm(this.model); - }, - - 'click @ui.permissions': function (e) { - e.preventDefault(); - Controller.showUserPermissions(this.model); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - Controller.showUserPasswordForm(this.model); + App.Controller.showNginxDeadForm(this.model); }, 'click @ui.delete': function (e) { e.preventDefault(); - Controller.showUserDeleteConfirm(this.model); - }, - - 'click @ui.login': function (e) { - e.preventDefault(); - - if (Cache.User.get('id') !== this.model.get('id')) { - this.ui.login.prop('disabled', true).addClass('btn-disabled'); - - Api.Users.loginAs(this.model.get('id')) - .then(res => { - Tokens.addToken(res.token, res.user.nickname || res.user.name); - window.location = '/'; - window.location.reload(); - }) - .catch(err => { - alert(err.message); - this.ui.login.prop('disabled', false).removeClass('btn-disabled'); - }); - } + App.Controller.showNginxDeadDeleteConfirm(this.model); } }, templateContext: { - isSelf: function () { - return Cache.User.get('id') === this.id; - } + canManage: App.Cache.User.canManage('dead_hosts') }, initialize: function () { diff --git a/src/frontend/js/app/nginx/dead/list/main.ejs b/src/frontend/js/app/nginx/dead/list/main.ejs index ce893413..9320b396 100644 --- a/src/frontend/js/app/nginx/dead/list/main.ejs +++ b/src/frontend/js/app/nginx/dead/list/main.ejs @@ -1,9 +1,10 @@   - Name - Email - Roles + <%- i18n('str', 'source') %> + <%- i18n('str', 'ssl') %> + <% if (canManage) { %>   + <% } %> diff --git a/src/frontend/js/app/nginx/dead/list/main.js b/src/frontend/js/app/nginx/dead/list/main.js index 80b7bd54..c1833739 100644 --- a/src/frontend/js/app/nginx/dead/list/main.js +++ b/src/frontend/js/app/nginx/dead/list/main.js @@ -1,8 +1,9 @@ 'use strict'; -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../../main'); +const ItemView = require('./item'); +const template = require('./main.ejs'); const TableBody = Mn.CollectionView.extend({ tagName: 'tbody', @@ -21,6 +22,10 @@ module.exports = Mn.View.extend({ } }, + templateContext: { + canManage: App.Cache.User.canManage('dead_hosts') + }, + onRender: function () { this.showChildView('body', new TableBody({ collection: this.collection diff --git a/src/frontend/js/app/nginx/dead/main.ejs b/src/frontend/js/app/nginx/dead/main.ejs index 53c0db77..9951fb79 100644 --- a/src/frontend/js/app/nginx/dead/main.ejs +++ b/src/frontend/js/app/nginx/dead/main.ejs @@ -1,10 +1,10 @@
-

404 Hosts

+

<%- i18n('dead-hosts', 'title') %>

<% if (showAddButton) { %> - Add 404 Host + <%- i18n('dead-hosts', 'add') %> <% } %>
diff --git a/src/frontend/js/app/nginx/dead/main.js b/src/frontend/js/app/nginx/dead/main.js index f2a8202e..151b8076 100644 --- a/src/frontend/js/app/nginx/dead/main.js +++ b/src/frontend/js/app/nginx/dead/main.js @@ -1,14 +1,12 @@ 'use strict'; const Mn = require('backbone.marionette'); +const App = require('../../main'); const DeadHostModel = require('../../../models/dead-host'); -const Api = require('../../api'); -const Cache = require('../../cache'); -const Controller = require('../../controller'); const ListView = require('./list/main'); const ErrorView = require('../../error/main'); -const template = require('./main.ejs'); const EmptyView = require('../../empty/main'); +const template = require('./main.ejs'); module.exports = Mn.View.extend({ id: 'nginx-dead', @@ -27,18 +25,18 @@ module.exports = Mn.View.extend({ events: { 'click @ui.add': function (e) { e.preventDefault(); - Controller.showNginxDeadForm(); + App.Controller.showNginxDeadForm(); } }, templateContext: { - showAddButton: Cache.User.canManage('dead_hosts') + showAddButton: App.Cache.User.canManage('dead_hosts') }, onRender: function () { let view = this; - Api.Nginx.DeadHosts.getAll() + App.Api.Nginx.DeadHosts.getAll(['owner']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) { @@ -46,15 +44,16 @@ module.exports = Mn.View.extend({ collection: new DeadHostModel.Collection(response) })); } else { - let manage = Cache.User.canManage('dead_hosts'); + let manage = App.Cache.User.canManage('dead_hosts'); view.showChildView('list_region', new EmptyView({ - title: 'There are no 404 Hosts', - subtitle: manage ? 'Why don\'t you create one?' : 'And you don\'t have permission to create one.', - link: manage ? 'Add 404 Host' : null, - btn_color: 'danger', - action: function () { - Controller.showNginxDeadForm(); + title: App.i18n('dead-hosts', 'empty'), + subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), + link: manage ? App.i18n('dead-hosts', 'add') : null, + btn_color: 'danger', + permission: 'dead_hosts', + action: function () { + App.Controller.showNginxDeadForm(); } })); } @@ -65,7 +64,7 @@ module.exports = Mn.View.extend({ code: err.code, message: err.message, retry: function () { - Controller.showNginxDead(); + App.Controller.showNginxDead(); } })); diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index 3c2ca76b..09369e4d 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -11,8 +11,8 @@ require('jquery-mask-plugin'); require('selectize'); module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', + template: template, + className: 'modal-dialog', max_file_size: 5120, ui: { @@ -60,19 +60,15 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); // Manipulate - data.forward_port = parseInt(data.forward_port, 10); - _.map(data, function (item, idx) { - if (typeof item === 'string' && item === '1') { - item = true; - } else if (typeof item === 'object' && item !== null) { - _.map(item, function (item2, idx2) { - if (typeof item2 === 'string' && item2 === '1') { - item[idx2] = true; - } - }); - } - data[idx] = item; - }); + data.forward_port = parseInt(data.forward_port, 10); + data.block_exploits = !!data.block_exploits; + data.caching_enabled = !!data.caching_enabled; + 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(','); diff --git a/src/frontend/js/app/nginx/redirection/form.js b/src/frontend/js/app/nginx/redirection/form.js index 1b8baa54..cc8dac80 100644 --- a/src/frontend/js/app/nginx/redirection/form.js +++ b/src/frontend/js/app/nginx/redirection/form.js @@ -58,18 +58,14 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); // Manipulate - _.map(data, function (item, idx) { - if (typeof item === 'string' && item === '1') { - item = true; - } else if (typeof item === 'object' && item !== null) { - _.map(item, function (item2, idx2) { - if (typeof item2 === 'string' && item2 === '1') { - item[idx2] = true; - } - }); - } - data[idx] = item; - }); + data.block_exploits = !!data.block_exploits; + data.preserve_path = !!data.preserve_path; + 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(','); diff --git a/src/frontend/js/app/nginx/stream/form.ejs b/src/frontend/js/app/nginx/stream/form.ejs index c1feee13..b0a72e48 100644 --- a/src/frontend/js/app/nginx/stream/form.ejs +++ b/src/frontend/js/app/nginx/stream/form.ejs @@ -9,7 +9,7 @@
- +
@@ -21,7 +21,7 @@
- +
@@ -38,10 +38,13 @@
+
+
<%- i18n('streams', 'forward-type-error') %>
+
diff --git a/src/frontend/js/app/nginx/stream/form.js b/src/frontend/js/app/nginx/stream/form.js index 143e54ed..260a8ce1 100644 --- a/src/frontend/js/app/nginx/stream/form.js +++ b/src/frontend/js/app/nginx/stream/form.js @@ -11,19 +11,24 @@ require('jquery-mask-plugin'); require('selectize'); module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - max_file_size: 5120, + template: template, + className: 'modal-dialog', ui: { form: 'form', forward_ip: 'input[name="forward_ip"]', + type_error: '.forward-type-error', buttons: '.modal-footer button', + switches: '.custom-switch-input', cancel: 'button.cancel', save: 'button.save' }, events: { + 'change @ui.switches': function () { + this.ui.type_error.hide(); + }, + 'click @ui.save': function (e) { e.preventDefault(); @@ -35,20 +40,16 @@ module.exports = Mn.View.extend({ let view = this; let data = this.ui.form.serializeJSON(); + if (!data.tcp_forwarding && !data.udp_forwarding) { + this.ui.type_error.show(); + return; + } + // Manipulate - data.forward_port = parseInt(data.forward_port, 10); - _.map(data, function (item, idx) { - if (typeof item === 'string' && item === '1') { - item = true; - } else if (typeof item === 'object' && item !== null) { - _.map(item, function (item2, idx2) { - if (typeof item2 === 'string' && item2 === '1') { - item[idx2] = true; - } - }); - } - data[idx] = item; - }); + data.incoming_port = parseInt(data.incoming_port, 10); + data.forwarding_port = parseInt(data.forwarding_port, 10); + data.tcp_forwarding = !!data.tcp_forwarding; + data.udp_forwarding = !!data.udp_forwarding; let method = App.Api.Nginx.Streams.create; let is_new = true; diff --git a/src/frontend/js/app/nginx/stream/list/item.ejs b/src/frontend/js/app/nginx/stream/list/item.ejs index b1aab6e4..b5850325 100644 --- a/src/frontend/js/app/nginx/stream/list/item.ejs +++ b/src/frontend/js/app/nginx/stream/list/item.ejs @@ -4,26 +4,25 @@ -
- <% domain_names.map(function(host) { - %> - <%- host %> - <% - }); - %> +
+ <%- incoming_port %>
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
-
<%- forward_ip %>:<%- forward_port %>
+
<%- forward_ip %>:<%- forwarding_port %>
-
<%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %>
- - -
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
+
+ <% if (tcp_forwarding) { %> + <%- i18n('streams', 'tcp') %> + <% } + if (udp_forwarding) { %> + <%- i18n('streams', 'udp') %> + <% } %> +
<% if (canManage) { %> @@ -31,7 +30,6 @@ diff --git a/src/frontend/js/app/nginx/stream/list/item.js b/src/frontend/js/app/nginx/stream/list/item.js index 52a201e8..074856db 100644 --- a/src/frontend/js/app/nginx/stream/list/item.js +++ b/src/frontend/js/app/nginx/stream/list/item.js @@ -16,17 +16,17 @@ module.exports = Mn.View.extend({ events: { 'click @ui.edit': function (e) { e.preventDefault(); - App.Controller.showNginxProxyForm(this.model); + App.Controller.showNginxStreamForm(this.model); }, 'click @ui.delete': function (e) { e.preventDefault(); - App.Controller.showNginxProxyDeleteConfirm(this.model); + App.Controller.showNginxStreamDeleteConfirm(this.model); } }, templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts') + canManage: App.Cache.User.canManage('streams') }, initialize: function () { diff --git a/src/frontend/js/app/nginx/stream/list/main.ejs b/src/frontend/js/app/nginx/stream/list/main.ejs index f2c64ea3..7ab11b1d 100644 --- a/src/frontend/js/app/nginx/stream/list/main.ejs +++ b/src/frontend/js/app/nginx/stream/list/main.ejs @@ -1,9 +1,8 @@   - <%- i18n('str', 'source') %> + <%- i18n('streams', 'incoming-port') %> <%- i18n('str', 'destination') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'access') %> + <%- i18n('streams', 'protocol') %> <% if (canManage) { %>   <% } %> diff --git a/src/frontend/js/app/nginx/stream/list/main.js b/src/frontend/js/app/nginx/stream/list/main.js index 64896c1f..905eaa9d 100644 --- a/src/frontend/js/app/nginx/stream/list/main.js +++ b/src/frontend/js/app/nginx/stream/list/main.js @@ -23,7 +23,7 @@ module.exports = Mn.View.extend({ }, templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts') + canManage: App.Cache.User.canManage('streams') }, onRender: function () { diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index 7856e4a4..e7552d3c 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -94,12 +94,27 @@ "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?" }, "dead-hosts": { - "title": "404 Hosts" + "title": "404 Hosts", + "empty": "There are no 404 Hosts", + "add": "Add 404 Host", + "form-title": "{id, select, undefined{New} other{Edit}} 404 Host" }, "streams": { "title": "Streams", "empty": "There are no Streams", - "add": "Add Stream" + "add": "Add Stream", + "form-title": "{id, select, undefined{New} other{Edit}} Stream", + "incoming-port": "Incoming Port", + "forward-ip": "Forward IP", + "forwarding-port": "Forward Port", + "tcp-forwarding": "TCP Forwarding", + "udp-forwarding": "UDP Forwarding", + "forward-type-error": "At least one type of protocol must be enabled", + "protocol": "Protocol", + "tcp": "TCP", + "udp": "UDP", + "delete": "Delete Stream", + "delete-confirm": "Are you sure you want to delete this Stream?" }, "access-lists": { "title": "Access Lists", diff --git a/src/frontend/js/models/dead-host.js b/src/frontend/js/models/dead-host.js index 41b89ee3..05656f9a 100644 --- a/src/frontend/js/models/dead-host.js +++ b/src/frontend/js/models/dead-host.js @@ -10,11 +10,13 @@ const model = Backbone.Model.extend({ id: 0, created_on: null, modified_on: null, - owner: null, - domain_name: '', + domain_names: [], ssl_enabled: false, ssl_provider: false, - meta: [] + ssl_forced: false, + meta: {}, + // The following are expansions: + owner: null }; } }); diff --git a/src/frontend/js/models/stream.js b/src/frontend/js/models/stream.js index e11ba3e6..221dd345 100644 --- a/src/frontend/js/models/stream.js +++ b/src/frontend/js/models/stream.js @@ -11,9 +11,9 @@ const model = Backbone.Model.extend({ created_on: null, modified_on: null, owner: null, - incoming_port: 3000, - forward_ip: '', - forwarding_port: 3000, + incoming_port: null, + forward_ip: null, + forwarding_port: null, tcp_forwarding: true, udp_forwarding: false, meta: {}