diff --git a/src/backend/internal/access-list.js b/src/backend/internal/access-list.js new file mode 100644 index 0000000..00f0120 --- /dev/null +++ b/src/backend/internal/access-list.js @@ -0,0 +1,183 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const accessListModel = require('../models/access_list'); + +function omissions () { + return ['is_deleted']; +} + +const internalAccessList = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('access_lists:create', data) + .then(access_data => { + // TODO + return {}; + }); + }, + + /** + * @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('access_lists:update', data.id) + .then(access_data => { + // TODO + return {}; + }); + }, + + /** + * @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 = {}; + } + + if (typeof data.id === 'undefined' || !data.id) { + data.id = access.token.get('attrs').id; + } + + return access.can('access_lists:get', data.id) + .then(access_data => { + let query = accessListModel + .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('access_lists:delete', data.id) + .then(() => { + return internalAccessList.get(access, {id: data.id}); + }) + .then(row => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return accessListModel + .query() + .where('id', row.id) + .patch({ + is_deleted: 1 + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Lists + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('access_lists:list') + .then(access_data => { + let query = accessListModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .allowEager('[owner]') + .orderBy('name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('name', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Integer} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = accessListModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then(row => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalAccessList; diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index dbcb4d5..649a3df 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -125,7 +125,7 @@ const internalProxyHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[permissions]') + .allowEager('[owner,access_list]') .first(); if (access_data.permission_visibility !== 'all') { diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index 0ec8ab8..75e5447 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -3,12 +3,210 @@ const _ = require('lodash'); const error = require('../lib/error'); const redirectionHostModel = require('../models/redirection_host'); +const internalHost = require('./host'); function omissions () { return ['is_deleted']; } -const internalProxyHost = { +const internalRedirectionHost = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('redirection_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 redirectionHostModel + .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('redirection_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, 'redirection', 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 internalRedirectionHost.get(access, {id: data.id}); + }) + .then(row => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return redirectionHostModel + .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 = {}; + } + + 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 + .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('redirection_hosts:delete', data.id) + .then(() => { + return internalRedirectionHost.get(access, {id: data.id}); + }) + .then(row => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return redirectionHostModel + .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 internalRedirectionHost.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 internalRedirectionHost.update(access, { + id: data.id, + meta: row.meta + }); + }) + .then(row => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); + }, /** * All Hosts @@ -26,6 +224,7 @@ const internalProxyHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) + .allowEager('[owner]') .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { @@ -44,6 +243,13 @@ const internalProxyHost = { } return query; + }) + .then(rows => { + rows.map(row => { + row.meta = internalHost.cleanMeta(row.meta); + }); + + return rows; }); }, @@ -71,4 +277,4 @@ const internalProxyHost = { } }; -module.exports = internalProxyHost; +module.exports = internalRedirectionHost; diff --git a/src/backend/lib/access/access_lists-create.json b/src/backend/lib/access/access_lists-create.json new file mode 100644 index 0000000..f2a91ff --- /dev/null +++ b/src/backend/lib/access/access_lists-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/access_lists-delete.json b/src/backend/lib/access/access_lists-delete.json new file mode 100644 index 0000000..f2a91ff --- /dev/null +++ b/src/backend/lib/access/access_lists-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/access_lists-get.json b/src/backend/lib/access/access_lists-get.json new file mode 100644 index 0000000..12203b3 --- /dev/null +++ b/src/backend/lib/access/access_lists-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/access_lists-list.json b/src/backend/lib/access/access_lists-list.json new file mode 100644 index 0000000..12203b3 --- /dev/null +++ b/src/backend/lib/access/access_lists-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/access_lists-update.json b/src/backend/lib/access/access_lists-update.json new file mode 100644 index 0000000..f2a91ff --- /dev/null +++ b/src/backend/lib/access/access_lists-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/redirection_hosts-create.json b/src/backend/lib/access/redirection_hosts-create.json new file mode 100644 index 0000000..b27c1f4 --- /dev/null +++ b/src/backend/lib/access/redirection_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/redirection_hosts-delete.json b/src/backend/lib/access/redirection_hosts-delete.json new file mode 100644 index 0000000..b27c1f4 --- /dev/null +++ b/src/backend/lib/access/redirection_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/redirection_hosts-get.json b/src/backend/lib/access/redirection_hosts-get.json new file mode 100644 index 0000000..227fc54 --- /dev/null +++ b/src/backend/lib/access/redirection_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/redirection_hosts-update.json b/src/backend/lib/access/redirection_hosts-update.json new file mode 100644 index 0000000..b27c1f4 --- /dev/null +++ b/src/backend/lib/access/redirection_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/routes/api/main.js b/src/backend/routes/api/main.js index 1f6fb92..c06c243 100644 --- a/src/backend/routes/api/main.js +++ b/src/backend/routes/api/main.js @@ -35,6 +35,7 @@ router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); router.use('/nginx/streams', require('./nginx/streams')); +router.use('/nginx/access-lists', require('./nginx/access_lists')); /** * API 404 for all other routes diff --git a/src/backend/routes/api/nginx/access_lists.js b/src/backend/routes/api/nginx/access_lists.js new file mode 100644 index 0000000..b514403 --- /dev/null +++ b/src/backend/routes/api/nginx/access_lists.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalAccessList = require('../../../internal/access-list'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/access-lists + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/access-lists + * + * Retrieve all access-lists + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then(data => { + return internalAccessList.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/access-lists + * + * Create a new access-list + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body) + .then(payload => { + return internalAccessList.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific access-list + * + * /api/nginx/access-lists/123 + */ +router + .route('/:host_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/access-lists/123 + * + * Retrieve a specific access-list + */ + .get((req, res, next) => { + validator({ + required: ['host_id'], + additionalProperties: false, + properties: { + host_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + host_id: req.params.host_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then(data => { + return internalAccessList.get(res.locals.access, { + id: parseInt(data.host_id, 10), + expand: data.expand + }); + }) + .then(row => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/access-lists/123 + * + * Update and existing access-list + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) + .then(payload => { + payload.id = parseInt(req.params.host_id, 10); + return internalAccessList.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/access-lists/123 + * + * Update and existing access-list + */ + .delete((req, res, next) => { + internalAccessList.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/src/backend/routes/api/nginx/redirection_hosts.js b/src/backend/routes/api/nginx/redirection_hosts.js index dbaf333..d2ec0d3 100644 --- a/src/backend/routes/api/nginx/redirection_hosts.js +++ b/src/backend/routes/api/nginx/redirection_hosts.js @@ -104,7 +104,7 @@ router }) .then(data => { return internalRedirectionHost.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/redirection-hosts#/links/2/schema'}, req.body) .then(payload => { - payload.id = req.params.host_id; + payload.id = parseInt(req.params.host_id, 10); return internalRedirectionHost.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing redirection-host */ .delete((req, res, next) => { - internalRedirectionHost.delete(res.locals.access, {id: req.params.host_id}) + internalRedirectionHost.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 redirection-host Certificates + * + * /api/nginx/redirection-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/redirection-hosts/123/certificates + * + * Upload certifications + */ + .post((req, res, next) => { + if (!req.files) { + res.status(400) + .send({error: 'No files were uploaded'}); + } else { + internalRedirectionHost.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/schema/definitions.json b/src/backend/schema/definitions.json index e064b4d..75c148b 100644 --- a/src/backend/schema/definitions.json +++ b/src/backend/schema/definitions.json @@ -116,6 +116,12 @@ "type": "integer", "minimum": 1 }, + "access_list_id": { + "description": "Access List ID", + "example": 1234, + "type": "integer", + "minimum": 0 + }, "name": { "type": "string", "minLength": 1, @@ -135,6 +141,12 @@ "minLength": 8, "maxLength": 255 }, + "domain_name": { + "description": "Domain Name", + "example": "jc21.com", + "type": "string", + "pattern": "^(?:[^.*]+\\.?)+[^.]$" + }, "domain_names": { "description": "Domain Names separated by a comma", "example": "*.jc21.com,blog.jc21.com", @@ -159,6 +171,16 @@ "ssl_provider": { "type": "string", "pattern": "^(letsencrypt|other)$" + }, + "block_exploits": { + "description": "Should we block common exploits", + "example": true, + "type": "boolean" + }, + "caching_enabled": { + "description": "Should we cache assets", + "example": true, + "type": "boolean" } } } diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json index e73ec0b..2e0a9d4 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -36,6 +36,15 @@ "ssl_provider": { "$ref": "../definitions.json#/definitions/ssl_provider" }, + "block_exploits": { + "$ref": "../definitions.json#/definitions/block_exploits" + }, + "caching_enabled": { + "$ref": "../definitions.json#/definitions/caching_enabled" + }, + "access_list_id": { + "$ref": "../definitions.json#/definitions/access_list_id" + }, "meta": { "type": "object", "additionalProperties": false, @@ -78,6 +87,15 @@ "ssl_provider": { "$ref": "#/definitions/ssl_provider" }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "caching_enabled": { + "$ref": "#/definitions/caching_enabled" + }, + "access_list_id": { + "$ref": "#/definitions/access_list_id" + }, "meta": { "$ref": "#/definitions/meta" } @@ -136,6 +154,15 @@ "ssl_provider": { "$ref": "#/definitions/ssl_provider" }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "caching_enabled": { + "$ref": "#/definitions/caching_enabled" + }, + "access_list_id": { + "$ref": "#/definitions/access_list_id" + }, "meta": { "$ref": "#/definitions/meta" } @@ -178,6 +205,15 @@ "ssl_provider": { "$ref": "#/definitions/ssl_provider" }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "caching_enabled": { + "$ref": "#/definitions/caching_enabled" + }, + "access_list_id": { + "$ref": "#/definitions/access_list_id" + }, "meta": { "$ref": "#/definitions/meta" } diff --git a/src/backend/schema/endpoints/redirection-hosts.json b/src/backend/schema/endpoints/redirection-hosts.json index 706d2ee..a66b50b 100644 --- a/src/backend/schema/endpoints/redirection-hosts.json +++ b/src/backend/schema/endpoints/redirection-hosts.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "endpoints/redirection-hosts", - "title": "Users", + "title": "Redirection Hosts", "description": "Endpoints relating to Redirection Hosts", "stability": "stable", "type": "object", @@ -15,49 +15,83 @@ "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 + "forward_domain_name": { + "$ref": "../definitions.json#/definitions/domain_name" }, - "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, + "preserve_path": { + "description": "Should the path be preserved", + "example": true, "type": "boolean" + }, + "ssl_enabled": { + "$ref": "../definitions.json#/definitions/ssl_enabled" + }, + "ssl_forced": { + "$ref": "../definitions.json#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "../definitions.json#/definitions/ssl_provider" + }, + "block_exploits": { + "$ref": "../definitions.json#/definitions/block_exploits" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "letsencrypt_email": { + "type": "string", + "format": "email" + }, + "letsencrypt_agree": { + "type": "boolean" + } + } + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "domain_names": { + "$ref": "#/definitions/domain_names" + }, + "forward_domain_name": { + "$ref": "#/definitions/forward_domain_name" + }, + "preserve_path": { + "$ref": "#/definitions/preserve_path" + }, + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" + }, + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "meta": { + "$ref": "#/definitions/meta" } }, "links": [ { "title": "List", - "description": "Returns a list of Users", - "href": "/users", + "description": "Returns a list of Redirection Hosts", + "href": "/nginx/redirection-hosts", "access": "private", "method": "GET", "rel": "self", @@ -73,8 +107,8 @@ }, { "title": "Create", - "description": "Creates a new User", - "href": "/users", + "description": "Creates a new Redirection Host", + "href": "/nginx/redirection-hosts", "access": "private", "method": "POST", "rel": "create", @@ -84,33 +118,33 @@ "schema": { "type": "object", "required": [ - "name", - "nickname", - "email" + "domain_names", + "forward_domain_name" ], "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_domain_name": { + "$ref": "#/definitions/forward_domain_name" }, - "email": { - "$ref": "#/definitions/email" + "preserve_path": { + "$ref": "#/definitions/preserve_path" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -122,8 +156,8 @@ }, { "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Updates a existing Redirection Host", + "href": "/nginx/redirection-hosts/{definitions.identity.example}", "access": "private", "method": "PUT", "rel": "update", @@ -133,20 +167,29 @@ "schema": { "type": "object", "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_domain_name": { + "$ref": "#/definitions/forward_domain_name" }, - "email": { - "$ref": "#/definitions/email" + "preserve_path": { + "$ref": "#/definitions/preserve_path" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "block_exploits": { + "$ref": "#/definitions/block_exploits" + }, + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -158,8 +201,8 @@ }, { "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Deletes a existing Redirection Host", + "href": "/nginx/redirection-hosts/{definitions.identity.example}", "access": "private", "method": "DELETE", "rel": "delete", @@ -170,34 +213,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/api.js b/src/frontend/js/app/api.js index 0a80b29..162e358 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -377,7 +377,7 @@ module.exports = { * @params {Promise} */ setCerts: function (id, form_data) { - return upload('nginx/redirection-hosts/' + id + '/certificates', form_data); + return FileUpload('nginx/redirection-hosts/' + id + '/certificates', form_data); } }, @@ -460,46 +460,46 @@ module.exports = { * @params {Promise} */ setCerts: function (id, form_data) { - return upload('nginx/dead-hosts/' + id + '/certificates', form_data); + return FileUpload('nginx/dead-hosts/' + id + '/certificates', form_data); } - } - }, - - AccessLists: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('access-lists', expand, query); }, - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'access-lists', data); - }, + AccessLists: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/access-lists', expand, query); + }, - /** - * @param {Object} data - * @param {Integer} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'access-lists/' + id, data); - }, + /** + * @param {Object} data + */ + create: function (data) { + return fetch('post', 'nginx/access-lists', data); + }, - /** - * @param {Integer} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'access-lists/' + id); - } + /** + * @param {Object} data + * @param {Integer} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'nginx/access-lists/' + id, data); + }, + + /** + * @param {Integer} id + * @returns {Promise} + */ + delete: function (id) { + return fetch('delete', 'nginx/access-lists/' + id); + } + }, }, AuditLog: { diff --git a/src/frontend/js/app/audit-log/main.ejs b/src/frontend/js/app/audit-log/main.ejs index b5440e6..acaa8b4 100644 --- a/src/frontend/js/app/audit-log/main.ejs +++ b/src/frontend/js/app/audit-log/main.ejs @@ -1,4 +1,5 @@
+

<%- i18n('audit-log', 'title') %>

diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 1438080..ab9d11d 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -174,6 +174,32 @@ module.exports = { } }, + /** + * Nginx Redirection Host Form + * + * @param [model] + */ + showNginxRedirectionForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { + require(['./main', './nginx/redirection/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + + /** + * Proxy Redirection Delete Confirm + * + * @param model + */ + showNginxRedirectionDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { + require(['./main', './nginx/redirection/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Nginx Stream Hosts */ @@ -216,6 +242,19 @@ module.exports = { } }, + /** + * Nginx Access List Form + * + * @param [model] + */ + showNginxAccessListForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { + require(['./main', './nginx/access/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Audit Log */ diff --git a/src/frontend/js/app/nginx/access/delete.ejs b/src/frontend/js/app/nginx/access/delete.ejs new file mode 100644 index 0000000..ba1b89e --- /dev/null +++ b/src/frontend/js/app/nginx/access/delete.ejs @@ -0,0 +1,19 @@ + diff --git a/src/frontend/js/app/nginx/access/delete.js b/src/frontend/js/app/nginx/access/delete.js new file mode 100644 index 0000000..91be703 --- /dev/null +++ b/src/frontend/js/app/nginx/access/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.ProxyHosts.delete(this.model.get('id')) + .then(() => { + App.Controller.showNginxAccess(); + 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/access/form.ejs b/src/frontend/js/app/nginx/access/form.ejs new file mode 100644 index 0000000..ebfeafe --- /dev/null +++ b/src/frontend/js/app/nginx/access/form.ejs @@ -0,0 +1,122 @@ + diff --git a/src/frontend/js/app/nginx/access/form.js b/src/frontend/js/app/nginx/access/form.js new file mode 100644 index 0000000..3c2ca76 --- /dev/null +++ b/src/frontend/js/app/nginx/access/form.js @@ -0,0 +1,195 @@ +'use strict'; + +const _ = require('underscore'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const ProxyHostModel = require('../../../models/proxy-host'); +const template = require('./form.ejs'); + +require('jquery-serializejson'); +require('jquery-mask-plugin'); +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"]', + forward_ip: 'input[name="forward_ip"]', + 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.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; + }); + + 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.ProxyHosts.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.ProxyHosts.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.ProxyHosts.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.showNginxProxy(); + } + }); + }) + .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.forward_ip.mask('099.099.099.099', { + clearIfNotMatch: true, + placeholder: '000.000.000.000' + }); + + 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 ProxyHostModel.Model(); + } + } +}); diff --git a/src/frontend/js/app/nginx/access/list/item.ejs b/src/frontend/js/app/nginx/access/list/item.ejs new file mode 100644 index 0000000..7a815a0 --- /dev/null +++ b/src/frontend/js/app/nginx/access/list/item.ejs @@ -0,0 +1,40 @@ + +
+ +
+ + +
+ <% domain_names.map(function(host) { + %> + <%- host %> + <% + }); + %> +
+
+ <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> +
+ + +
<%- forward_ip %>:<%- forward_port %>
+ + +
<%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %>
+ + +
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
+ +<% if (canManage) { %> + + + +<% } %> \ No newline at end of file diff --git a/src/frontend/js/app/nginx/access/list/item.js b/src/frontend/js/app/nginx/access/list/item.js new file mode 100644 index 0000000..52a201e --- /dev/null +++ b/src/frontend/js/app/nginx/access/list/item.js @@ -0,0 +1,35 @@ +'use strict'; + +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', + delete: 'a.delete' + }, + + events: { + 'click @ui.edit': function (e) { + e.preventDefault(); + App.Controller.showNginxProxyForm(this.model); + }, + + 'click @ui.delete': function (e) { + e.preventDefault(); + App.Controller.showNginxProxyDeleteConfirm(this.model); + } + }, + + templateContext: { + canManage: App.Cache.User.canManage('proxy_hosts') + }, + + initialize: function () { + this.listenTo(this.model, 'change', this.render); + } +}); diff --git a/src/frontend/js/app/nginx/access/list/main.ejs b/src/frontend/js/app/nginx/access/list/main.ejs new file mode 100644 index 0000000..f2c64ea --- /dev/null +++ b/src/frontend/js/app/nginx/access/list/main.ejs @@ -0,0 +1,13 @@ + +   + <%- i18n('str', 'source') %> + <%- i18n('str', 'destination') %> + <%- i18n('str', 'ssl') %> + <%- i18n('str', 'access') %> + <% if (canManage) { %> +   + <% } %> + + + + diff --git a/src/frontend/js/app/nginx/access/list/main.js b/src/frontend/js/app/nginx/access/list/main.js new file mode 100644 index 0000000..d5b7aa7 --- /dev/null +++ b/src/frontend/js/app/nginx/access/list/main.js @@ -0,0 +1,34 @@ +'use strict'; + +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', + childView: ItemView +}); + +module.exports = Mn.View.extend({ + tagName: 'table', + className: 'table table-hover table-outline table-vcenter text-nowrap card-table', + template: template, + + regions: { + body: { + el: 'tbody', + replaceElement: true + } + }, + + templateContext: { + canManage: App.Cache.User.canManage('access_lists') + }, + + onRender: function () { + this.showChildView('body', new TableBody({ + collection: this.collection + })); + } +}); diff --git a/src/frontend/js/app/nginx/access/main.ejs b/src/frontend/js/app/nginx/access/main.ejs index dd8b772..140cd49 100644 --- a/src/frontend/js/app/nginx/access/main.ejs +++ b/src/frontend/js/app/nginx/access/main.ejs @@ -1 +1,19 @@ -access \ No newline at end of file +
+
+
+

<%- i18n('access-lists', 'title') %>

+
+ <% if (showAddButton) { %> + <%- i18n('access-lists', 'add') %> + <% } %> +
+
+
+
+
+
+ +
+
+
+
diff --git a/src/frontend/js/app/nginx/access/main.js b/src/frontend/js/app/nginx/access/main.js index 73dd931..0214bd7 100644 --- a/src/frontend/js/app/nginx/access/main.js +++ b/src/frontend/js/app/nginx/access/main.js @@ -1,9 +1,77 @@ 'use strict'; -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const AccessListModel = require('../../../models/access-list'); +const ListView = require('./list/main'); +const ErrorView = require('../../error/main'); +const EmptyView = require('../../empty/main'); +const template = require('./main.ejs'); module.exports = Mn.View.extend({ + id: 'nginx-access', template: template, - id: 'nginx-access' + + ui: { + list_region: '.list-region', + add: '.add-item', + dimmer: '.dimmer' + }, + + regions: { + list_region: '@ui.list_region' + }, + + events: { + 'click @ui.add': function (e) { + e.preventDefault(); + App.Controller.showNginxAccessListForm(); + } + }, + + templateContext: { + showAddButton: App.Cache.User.canManage('access_lists') + }, + + onRender: function () { + let view = this; + + App.Api.Nginx.AccessLists.getAll(['owner']) + .then(response => { + if (!view.isDestroyed()) { + if (response && response.length) { + view.showChildView('list_region', new ListView({ + collection: new AccessListModel.Collection(response) + })); + } else { + let manage = App.Cache.User.canManage('access_lists'); + + view.showChildView('list_region', new EmptyView({ + title: App.i18n('access-lists', 'empty'), + subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), + link: manage ? App.i18n('access-lists', 'add') : null, + btn_color: 'teal', + permission: 'access_lists', + action: function () { + App.Controller.showNginxAccessListForm(); + } + })); + } + } + }) + .catch(err => { + view.showChildView('list_region', new ErrorView({ + code: err.code, + message: err.message, + retry: function () { + App.Controller.showNginxAccess(); + } + })); + + console.error(err); + }) + .then(() => { + view.ui.dimmer.removeClass('active'); + }); + } }); diff --git a/src/frontend/js/app/nginx/proxy/delete.ejs b/src/frontend/js/app/nginx/proxy/delete.ejs index 25d3a2e..61f1ca1 100644 --- a/src/frontend/js/app/nginx/proxy/delete.ejs +++ b/src/frontend/js/app/nginx/proxy/delete.ejs @@ -8,10 +8,9 @@
<%= i18n('proxy-hosts', 'delete-confirm', {domains: domain_names.join(', ')}) %> - Are you sure you want to delete the Proxy host for: <%- domain_names.join(', ') %>? <% if (ssl_enabled) { %>

- <%- i18n('proxy-hosts', 'delete-ssl') %> + <%- i18n('ssl', 'delete-ssl') %> <% } %>
diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index ebfeafe..2560dc9 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -32,6 +32,32 @@
+
+
+ +
+
+
+
+ +
+
+
+
+ + +
+
diff --git a/src/frontend/js/app/nginx/redirection/delete.ejs b/src/frontend/js/app/nginx/redirection/delete.ejs new file mode 100644 index 0000000..ff848f3 --- /dev/null +++ b/src/frontend/js/app/nginx/redirection/delete.ejs @@ -0,0 +1,23 @@ + diff --git a/src/frontend/js/app/nginx/redirection/delete.js b/src/frontend/js/app/nginx/redirection/delete.js new file mode 100644 index 0000000..93b8691 --- /dev/null +++ b/src/frontend/js/app/nginx/redirection/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.RedirectionHosts.delete(this.model.get('id')) + .then(() => { + App.Controller.showNginxRedirection(); + 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/redirection/form.ejs b/src/frontend/js/app/nginx/redirection/form.ejs new file mode 100644 index 0000000..27b109a --- /dev/null +++ b/src/frontend/js/app/nginx/redirection/form.ejs @@ -0,0 +1,134 @@ + diff --git a/src/frontend/js/app/nginx/redirection/form.js b/src/frontend/js/app/nginx/redirection/form.js new file mode 100644 index 0000000..1b8baa5 --- /dev/null +++ b/src/frontend/js/app/nginx/redirection/form.js @@ -0,0 +1,187 @@ +'use strict'; + +const _ = require('underscore'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const RedirectionHostModel = require('../../../models/redirection-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 + _.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; + }); + + 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.RedirectionHosts.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.RedirectionHosts.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.RedirectionHosts.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.showNginxRedirection(); + } + }); + }) + .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 RedirectionHostModel.Model(); + } + } +}); diff --git a/src/frontend/js/app/nginx/redirection/main.js b/src/frontend/js/app/nginx/redirection/main.js index b6e013e..ca82c85 100644 --- a/src/frontend/js/app/nginx/redirection/main.js +++ b/src/frontend/js/app/nginx/redirection/main.js @@ -1,17 +1,15 @@ 'use strict'; const Mn = require('backbone.marionette'); +const App = require('../../main'); const RedirectionHostModel = require('../../../models/redirection-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-redirections', + id: 'nginx-redirection', template: template, ui: { @@ -27,34 +25,35 @@ module.exports = Mn.View.extend({ events: { 'click @ui.add': function (e) { e.preventDefault(); - Controller.showNginxRedirectionForm(); + App.Controller.showNginxProxyForm(); } }, templateContext: { - showAddButton: Cache.User.canManage('redirection_hosts') + showAddButton: App.Cache.User.canManage('proxy_hosts') }, onRender: function () { let view = this; - Api.Nginx.RedirectionHosts.getAll() + App.Api.Nginx.ProxyHosts.getAll(['owner', 'access_list']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) { view.showChildView('list_region', new ListView({ - collection: new RedirectionHostModel.Collection(response) + collection: new ProxyHostModel.Collection(response) })); } else { - let manage = Cache.User.canManage('redirection_hosts'); + let manage = App.Cache.User.canManage('proxy_hosts'); view.showChildView('list_region', new EmptyView({ - title: 'There are no Redirection Hosts', - subtitle: manage ? 'Why don\'t you create one?' : 'And you don\'t have permission to create one.', - link: manage ? 'Add Redirection Host' : null, - btn_color: 'yellow', - action: function () { - Controller.showNginxRedirectionForm(); + title: App.i18n('proxy-hosts', 'empty'), + subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), + link: manage ? App.i18n('proxy-hosts', 'add') : null, + btn_color: 'success', + permission: 'proxy_hosts', + action: function () { + App.Controller.showNginxProxyForm(); } })); } @@ -65,7 +64,7 @@ module.exports = Mn.View.extend({ code: err.code, message: err.message, retry: function () { - Controller.showNginxRedirection(); + App.Controller.showNginxProxy(); } })); diff --git a/src/frontend/js/app/users/main.ejs b/src/frontend/js/app/users/main.ejs index 8a56778..da5e0d4 100644 --- a/src/frontend/js/app/users/main.ejs +++ b/src/frontend/js/app/users/main.ejs @@ -1,4 +1,5 @@
+

<%- i18n('users', 'title') %>

diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index ef5a0df..926b215 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -61,14 +61,17 @@ "domain-names": "Domain Names", "cert-provider": "Certificate Provider", "other-certificate": "Certificate", - "other-certificate-key": "Certificate Key" + "other-certificate-key": "Certificate Key", + "block-exploits": "Block Common Exploits", + "caching-enabled": "Cache Assets" }, "ssl": { "letsencrypt": "Let's Encrypt", "other": "Other", "none": "HTTP only", "letsencrypt-email": "Email Address for Let's Encrypt", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service" + "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", + "delete-ssl": "The SSL certificates attached will be removed, this action cannot be recovered." }, "proxy-hosts": { "title": "Proxy Hosts", @@ -78,11 +81,15 @@ "forward-ip": "Forward IP", "forward-port": "Forward Port", "delete": "Delete Proxy Host", - "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", - "delete-ssl": "The SSL certificates attached will be removed, this action cannot be recovered." + "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?" }, "redirection-hosts": { - "title": "Redirection Hosts" + "title": "Redirection Hosts", + "form-title": "{id, select, undefined{New} other{Edit}} Redirection Host", + "forward-domain": "Forward Domain", + "preserve-path": "Preserve Path", + "delete": "Delete Proxy Host", + "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?" }, "dead-hosts": { "title": "404 Hosts" @@ -91,7 +98,12 @@ "title": "Streams" }, "access-lists": { - "title": "Access Lists" + "title": "Access Lists", + "empty": "There are no Access Lists", + "add": "Add Access List", + "delete": "Delete Access List", + "delete-confirm": "Are you sure you want to delete this access list? Any hosts using it will need to be updated later.", + "public": "Publicly Accessible" }, "users": { "title": "Users", diff --git a/src/frontend/js/models/access-list.js b/src/frontend/js/models/access-list.js new file mode 100644 index 0000000..ae0e7d6 --- /dev/null +++ b/src/frontend/js/models/access-list.js @@ -0,0 +1,24 @@ +'use strict'; + +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function () { + return { + id: 0, + created_on: null, + modified_on: null, + // The following are expansions: + owner: null + }; + } +}); + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model: model + }) +}; diff --git a/src/frontend/js/models/proxy-host.js b/src/frontend/js/models/proxy-host.js index 1127208..dec8daa 100644 --- a/src/frontend/js/models/proxy-host.js +++ b/src/frontend/js/models/proxy-host.js @@ -13,7 +13,7 @@ const model = Backbone.Model.extend({ domain_names: [], forward_ip: '', forward_port: null, - access_list_id: null, + access_list_id: 0, ssl_enabled: false, ssl_provider: false, ssl_forced: false, diff --git a/src/frontend/js/models/redirection-host.js b/src/frontend/js/models/redirection-host.js index 131fc94..c731fd8 100644 --- a/src/frontend/js/models/redirection-host.js +++ b/src/frontend/js/models/redirection-host.js @@ -10,14 +10,16 @@ const model = Backbone.Model.extend({ id: 0, created_on: null, modified_on: null, - owner: null, - domain_name: '', + domain_names: [], forward_domain_name: '', - preserve_path: false, + preserve_path: true, ssl_enabled: false, ssl_provider: false, + ssl_forced: false, block_exploits: false, - meta: [] + meta: {}, + // The following are expansions: + owner: null }; } });