From 1c57ccdc87ae7bf00c80ecd1449d843b12c489c8 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 2 Aug 2018 19:48:47 +1000 Subject: [PATCH] Certificates ui section and permissions --- docker-compose.yml | 2 + rootfs/etc/nginx/mime.types | 96 +++++++++ rootfs/etc/services.d/nginx/run | 6 +- src/backend/index.js | 4 + src/backend/internal/certificate.js | 183 ++++++++++++++++ src/backend/internal/dead-host.js | 17 +- src/backend/internal/nginx.js | 124 ++++++++++- src/backend/internal/proxy-host.js | 19 +- src/backend/internal/redirection-host.js | 17 +- src/backend/internal/ssl.js | 3 +- src/backend/internal/stream.js | 17 +- src/backend/internal/user.js | 3 +- src/backend/lib/access.js | 3 +- .../lib/access/certificates-create.json | 23 +++ .../lib/access/certificates-delete.json | 23 +++ src/backend/lib/access/certificates-get.json | 23 +++ src/backend/lib/access/certificates-list.json | 23 +++ .../lib/access/certificates-update.json | 23 +++ .../migrations/20180618015850_initial.js | 15 ++ src/backend/models/certificate.js | 52 +++++ src/backend/routes/api/main.js | 1 + src/backend/routes/api/nginx/certificates.js | 150 ++++++++++++++ src/backend/schema/endpoints/users.json | 4 + src/backend/setup.js | 3 +- src/backend/templates/dead_host.conf | 26 ++- src/backend/templates/letsencrypt.conf | 9 +- src/backend/templates/proxy_host.conf | 56 +++-- src/backend/templates/redirection_host.conf | 40 ++-- src/backend/templates/stream.conf | 21 +- src/frontend/js/app/api.js | 37 ++++ src/frontend/js/app/controller.js | 14 ++ src/frontend/js/app/dashboard/main.js | 4 +- src/frontend/js/app/nginx/access/delete.js | 4 +- .../js/app/nginx/certificates/delete.ejs | 19 ++ .../js/app/nginx/certificates/delete.js | 34 +++ .../js/app/nginx/certificates/form.ejs | 122 +++++++++++ .../js/app/nginx/certificates/form.js | 195 ++++++++++++++++++ .../js/app/nginx/certificates/list/item.ejs | 40 ++++ .../js/app/nginx/certificates/list/item.js | 35 ++++ .../js/app/nginx/certificates/list/main.ejs | 13 ++ .../js/app/nginx/certificates/list/main.js | 34 +++ .../js/app/nginx/certificates/main.ejs | 20 ++ .../js/app/nginx/certificates/main.js | 83 ++++++++ src/frontend/js/app/nginx/dead/delete.js | 2 - src/frontend/js/app/nginx/dead/list/item.ejs | 11 + src/frontend/js/app/nginx/dead/list/item.js | 10 +- src/frontend/js/app/nginx/dead/list/main.ejs | 1 + src/frontend/js/app/nginx/proxy/delete.js | 2 - src/frontend/js/app/nginx/proxy/list/item.ejs | 11 + src/frontend/js/app/nginx/proxy/list/item.js | 10 +- src/frontend/js/app/nginx/proxy/list/main.ejs | 1 + .../js/app/nginx/redirection/delete.js | 2 - .../js/app/nginx/redirection/list/item.ejs | 11 + .../js/app/nginx/redirection/list/item.js | 10 +- .../js/app/nginx/redirection/list/main.ejs | 1 + src/frontend/js/app/nginx/stream/delete.js | 2 - .../js/app/nginx/stream/list/item.ejs | 11 + src/frontend/js/app/nginx/stream/list/item.js | 10 +- .../js/app/nginx/stream/list/main.ejs | 1 + src/frontend/js/app/router.js | 19 +- src/frontend/js/app/ui/menu/main.ejs | 7 +- src/frontend/js/app/user/permissions.ejs | 2 +- src/frontend/js/app/user/permissions.js | 3 +- src/frontend/js/i18n/messages.json | 15 +- src/frontend/js/models/certificate.js | 24 +++ 65 files changed, 1697 insertions(+), 109 deletions(-) create mode 100644 rootfs/etc/nginx/mime.types create mode 100644 src/backend/internal/certificate.js create mode 100644 src/backend/lib/access/certificates-create.json create mode 100644 src/backend/lib/access/certificates-delete.json create mode 100644 src/backend/lib/access/certificates-get.json create mode 100644 src/backend/lib/access/certificates-list.json create mode 100644 src/backend/lib/access/certificates-update.json create mode 100644 src/backend/models/certificate.js create mode 100644 src/backend/routes/api/nginx/certificates.js create mode 100644 src/frontend/js/app/nginx/certificates/delete.ejs create mode 100644 src/frontend/js/app/nginx/certificates/delete.js create mode 100644 src/frontend/js/app/nginx/certificates/form.ejs create mode 100644 src/frontend/js/app/nginx/certificates/form.js create mode 100644 src/frontend/js/app/nginx/certificates/list/item.ejs create mode 100644 src/frontend/js/app/nginx/certificates/list/item.js create mode 100644 src/frontend/js/app/nginx/certificates/list/main.ejs create mode 100644 src/frontend/js/app/nginx/certificates/list/main.js create mode 100644 src/frontend/js/app/nginx/certificates/main.ejs create mode 100644 src/frontend/js/app/nginx/certificates/main.js create mode 100644 src/frontend/js/models/certificate.js diff --git a/docker-compose.yml b/docker-compose.yml index f75abf4..ac0f64f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. version: "2" services: app: @@ -12,6 +13,7 @@ services: volumes: - ./data/letsencrypt:/etc/letsencrypt - .:/app + - ./rootfs/etc/nginx:/etc/nginx working_dir: /app depends_on: - db diff --git a/rootfs/etc/nginx/mime.types b/rootfs/etc/nginx/mime.types new file mode 100644 index 0000000..7c7cdef --- /dev/null +++ b/rootfs/etc/nginx/mime.types @@ -0,0 +1,96 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/rootfs/etc/services.d/nginx/run b/rootfs/etc/services.d/nginx/run index 7bf1449..20e756f 100755 --- a/rootfs/etc/services.d/nginx/run +++ b/rootfs/etc/services.d/nginx/run @@ -1,10 +1,12 @@ #!/usr/bin/with-contenv bash -mkdir -p /tmp/nginx \ +mkdir -p /tmp/nginx/body \ + /var/log/nginx \ /data/{nginx,logs,access} \ /data/nginx/{proxy_host,redirection_host,stream,dead_host} \ /var/lib/nginx/cache/{public,private} +touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log chown root /tmp/nginx -exec nginx +exec nginx diff --git a/src/backend/index.js b/src/backend/index.js index 0aae9bf..0de363a 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -9,6 +9,7 @@ function appStart () { const setup = require('./setup'); const app = require('./app'); const apiValidator = require('./lib/validator/api'); + const internalSsl = require('./internal/ssl'); return migrate.latest() .then(() => { @@ -18,6 +19,9 @@ function appStart () { return apiValidator.loadSchemas; }) .then(() => { + + internalSsl.initTimer(); + const server = app.listen(81, () => { logger.info('PID ' + process.pid + ' listening on port 81 ...'); diff --git a/src/backend/internal/certificate.js b/src/backend/internal/certificate.js new file mode 100644 index 0000000..cb714c0 --- /dev/null +++ b/src/backend/internal/certificate.js @@ -0,0 +1,183 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const certificateModel = require('../models/certificate'); + +function omissions () { + return ['is_deleted']; +} + +const internalCertificate = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('certificates: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('certificates: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('certificates:get', data.id) + .then(access_data => { + let query = certificateModel + .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('certificates:delete', data.id) + .then(() => { + return internalCertificate.get(access, {id: data.id}); + }) + .then(row => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return certificateModel + .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('certificates:list') + .then(access_data => { + let query = certificateModel + .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 = certificateModel + .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 = internalCertificate; diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index 8630f8d..dbcd537 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const deadHostModel = require('../models/dead_host'); const internalHost = require('./host'); +const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log'); function omissions () { @@ -49,6 +50,13 @@ const internalDeadHost = { .omit(omissions()) .insertAndFetch(data); }) + .then(row => { + // Configure nginx + return internalNginx.configure(deadHostModel, 'dead_host', row) + .then(() => { + return internalDeadHost.get(access, {id: row.id, expand: ['owner']}); + }); + }) .then(row => { // Add to audit log return internalAuditLog.add(access, { @@ -58,7 +66,7 @@ const internalDeadHost = { meta: data }) .then(() => { - return _.omit(row, omissions()); + return row; }); }); }, @@ -192,6 +200,13 @@ const internalDeadHost = { .patch({ is_deleted: 1 }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig('dead_host', row) + .then(() => { + return internalNginx.reload(); + }); + }) .then(() => { // Add to audit log row.meta = internalHost.cleanMeta(row.meta); diff --git a/src/backend/internal/nginx.js b/src/backend/internal/nginx.js index de163ff..c020958 100644 --- a/src/backend/internal/nginx.js +++ b/src/backend/internal/nginx.js @@ -1,18 +1,94 @@ 'use strict'; -const fs = require('fs'); -const Liquid = require('liquidjs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); +const _ = require('lodash'); +const fs = require('fs'); +const Liquid = require('liquidjs'); +const logger = require('../logger').nginx; +const utils = require('../lib/utils'); +const error = require('../lib/error'); +const internalSsl = require('./ssl'); +const debug_mode = process.env.NODE_ENV !== 'production'; const internalNginx = { + /** + * This will: + * - test the nginx config first to make sure it's OK + * - create / recreate the config for the host + * - test again + * - IF OK: update the meta with online status + * - IF BAD: update the meta with offline status and remove the config entirely + * - then reload nginx + * + * @param {Object} model + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + configure: (model, host_type, host) => { + return internalNginx.test() + .then(() => { + // Nginx is OK + // We're deleting this config regardless. + return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all + }) + .then(() => { + if (host.ssl && !internalSsl.hasValidSslCerts(host_type, host)) { + return internalSsl.configureSsl(host_type, host); + } + }) + .then(() => { + return internalNginx.generateConfig(host_type, host); + }) + .then(() => { + // Test nginx again and update meta with result + return internalNginx.test() + .then(() => { + // nginx is ok + return model + .query() + .where('id', host.id) + .patch({ + meta: _.assign({}, host.meta, { + nginx_online: true, + nginx_err: null + }) + }); + }) + .catch(err => { + + if (debug_mode) { + logger.error('Nginx test failed:', err.message); + } + + // config is bad, update meta and delete config + return model + .query() + .where('id', host.id) + .patch({ + meta: _.assign({}, host.meta, { + nginx_online: false, + nginx_err: err.message + }) + }) + .then(() => { + return internalNginx.deleteConfig(host_type, host, true); + }); + }); + }) + .then(() => { + return internalNginx.reload(); + }); + }, + /** * @returns {Promise} */ test: () => { - logger.info('Testing Nginx configuration'); + if (debug_mode) { + logger.info('Testing Nginx configuration'); + } + return utils.exec('/usr/sbin/nginx -t'); }, @@ -43,8 +119,13 @@ const internalNginx = { * @returns {Promise} */ generateConfig: (host_type, host) => { + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + + if (debug_mode) { + logger.info('Generating ' + host_type + ' Config:', host); + } + let renderEngine = Liquid(); - host_type = host_type.replace(new RegExp('-', 'g'), '_'); return new Promise((resolve, reject) => { let template = null; @@ -56,14 +137,23 @@ const internalNginx = { return; } - return renderEngine + renderEngine .parseAndRender(template, host) .then(config_text => { fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - return true; + + if (debug_mode) { + logger.success('Wrote config:', filename, config_text); + } + + resolve(true); }) .catch(err => { - throw new error.ConfigurationError(err.message); + if (debug_mode) { + logger.warn('Could not write ' + filename + ':', err.message); + } + + reject(new error.ConfigurationError(err.message)); }); }); }, @@ -75,10 +165,22 @@ const internalNginx = { * @returns {Promise} */ deleteConfig: (host_type, host, throw_errors) => { + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + return new Promise((resolve, reject) => { try { - fs.unlinkSync(internalNginx.getConfigName(host_type, host.id)); + let config_file = internalNginx.getConfigName(host_type, host.id); + + if (debug_mode) { + logger.warn('Deleting nginx config: ' + config_file); + } + + fs.unlinkSync(config_file); } catch (err) { + if (debug_mode) { + logger.warn('Could not delete config:', err.message); + } + if (throw_errors) { reject(err); } diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 391d6ac..f457904 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const proxyHostModel = require('../models/proxy_host'); const internalHost = require('./host'); +const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log'); function omissions () { @@ -50,6 +51,15 @@ const internalProxyHost = { .insertAndFetch(data); }) .then(row => { + // Configure nginx + return internalNginx.configure(proxyHostModel, 'proxy_host', row) + .then(() => { + return internalProxyHost.get(access, {id: row.id, expand: ['owner']}); + }); + }) + .then(row => { + data.meta = _.assign({}, data.meta || {}, row.meta); + // Add to audit log return internalAuditLog.add(access, { action: 'created', @@ -58,7 +68,7 @@ const internalProxyHost = { meta: data }) .then(() => { - return _.omit(row, omissions()); + return row; }); }); }, @@ -192,6 +202,13 @@ const internalProxyHost = { .patch({ is_deleted: 1 }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig('proxy_host', row) + .then(() => { + return internalNginx.reload(); + }); + }) .then(() => { // Add to audit log row.meta = internalHost.cleanMeta(row.meta); diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index 32ecd64..b2ca2bf 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const redirectionHostModel = require('../models/redirection_host'); const internalHost = require('./host'); +const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log'); function omissions () { @@ -49,6 +50,13 @@ const internalRedirectionHost = { .omit(omissions()) .insertAndFetch(data); }) + .then(row => { + // Configure nginx + return internalNginx.configure(redirectionHostModel, 'redirection_host', row) + .then(() => { + return internalRedirectionHost.get(access, {id: row.id, expand: ['owner']}); + }); + }) .then(row => { // Add to audit log return internalAuditLog.add(access, { @@ -58,7 +66,7 @@ const internalRedirectionHost = { meta: data }) .then(() => { - return _.omit(row, omissions()); + return row; }); }); }, @@ -192,6 +200,13 @@ const internalRedirectionHost = { .patch({ is_deleted: 1 }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig('redirection_host', row) + .then(() => { + return internalNginx.reload(); + }); + }) .then(() => { // Add to audit log row.meta = internalHost.cleanMeta(row.meta); diff --git a/src/backend/internal/ssl.js b/src/backend/internal/ssl.js index efa0930..714feff 100644 --- a/src/backend/internal/ssl.js +++ b/src/backend/internal/ssl.js @@ -17,6 +17,7 @@ const internalSsl = { interval_processing: false, initTimer: () => { + logger.info('Let\'s Encrypt Renewal Timer initialized'); internalSsl.interval = setInterval(internalSsl.processExpiringHosts, internalSsl.interval_timeout); }, @@ -51,7 +52,7 @@ const internalSsl = { */ hasValidSslCerts: (host_type, host) => { host_type = host_type.replace(new RegExp('-', 'g'), '_'); - let le_path = '/etc/letsencrypt/live/' + host_type + '_' + host.id; + let le_path = '/etc/letsencrypt/live/' + host_type + '-' + host.id; return fs.existsSync(le_path + '/fullchain.pem') && fs.existsSync(le_path + '/privkey.pem'); }, diff --git a/src/backend/internal/stream.js b/src/backend/internal/stream.js index bb8c9c3..91d104f 100644 --- a/src/backend/internal/stream.js +++ b/src/backend/internal/stream.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const streamModel = require('../models/stream'); +const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log'); function omissions () { @@ -31,6 +32,13 @@ const internalStream = { .omit(omissions()) .insertAndFetch(data); }) + .then(row => { + // Configure nginx + return internalNginx.configure(streamModel, 'stream', row) + .then(() => { + return internalStream.get(access, {id: row.id, expand: ['owner']}); + }); + }) .then(row => { // Add to audit log return internalAuditLog.add(access, { @@ -40,7 +48,7 @@ const internalStream = { meta: data }) .then(() => { - return _.omit(row, omissions()); + return row; }); }); }, @@ -153,6 +161,13 @@ const internalStream = { .patch({ is_deleted: 1 }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig('stream', row) + .then(() => { + return internalNginx.reload(); + }); + }) .then(() => { // Add to audit log return internalAuditLog.add(access, { diff --git a/src/backend/internal/user.js b/src/backend/internal/user.js index 6a91e98..0068881 100644 --- a/src/backend/internal/user.js +++ b/src/backend/internal/user.js @@ -70,7 +70,8 @@ const internalUser = { redirection_hosts: 'manage', dead_hosts: 'manage', streams: 'manage', - access_lists: 'manage' + access_lists: 'manage', + certificates: 'manage' }) .then(() => { return internalUser.get(access, {id: user.id, expand: ['permissions']}); diff --git a/src/backend/lib/access.js b/src/backend/lib/access.js index 81dc768..6b9ab8b 100644 --- a/src/backend/lib/access.js +++ b/src/backend/lib/access.js @@ -262,7 +262,8 @@ module.exports = function (token_string) { permission_redirection_hosts: permissions.redirection_hosts, permission_dead_hosts: permissions.dead_hosts, permission_streams: permissions.streams, - permission_access_lists: permissions.access_lists + permission_access_lists: permissions.access_lists, + permission_certificates: permissions.certificates } }; diff --git a/src/backend/lib/access/certificates-create.json b/src/backend/lib/access/certificates-create.json new file mode 100644 index 0000000..3eea8a2 --- /dev/null +++ b/src/backend/lib/access/certificates-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/certificates-delete.json b/src/backend/lib/access/certificates-delete.json new file mode 100644 index 0000000..3eea8a2 --- /dev/null +++ b/src/backend/lib/access/certificates-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/certificates-get.json b/src/backend/lib/access/certificates-get.json new file mode 100644 index 0000000..8966a4a --- /dev/null +++ b/src/backend/lib/access/certificates-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/certificates-list.json b/src/backend/lib/access/certificates-list.json new file mode 100644 index 0000000..8966a4a --- /dev/null +++ b/src/backend/lib/access/certificates-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/certificates-update.json b/src/backend/lib/access/certificates-update.json new file mode 100644 index 0000000..3eea8a2 --- /dev/null +++ b/src/backend/lib/access/certificates-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$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 153f353..2333732 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -55,6 +55,7 @@ exports.up = function (knex/*, Promise*/) { table.string('dead_hosts').notNull(); table.string('streams').notNull(); table.string('access_lists').notNull(); + table.string('certificates').notNull(); table.unique('user_id'); }); }) @@ -147,6 +148,20 @@ exports.up = function (knex/*, Promise*/) { .then(() => { logger.info('[' + migrate_name + '] access_list Table created'); + return knex.schema.createTable('certificate', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('name').notNull(); + // TODO + table.json('meta').notNull(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] certificate Table created'); + return knex.schema.createTable('access_list_auth', table => { table.increments().primary(); table.dateTime('created_on').notNull(); diff --git a/src/backend/models/certificate.js b/src/backend/models/certificate.js new file mode 100644 index 0000000..7becf01 --- /dev/null +++ b/src/backend/models/certificate.js @@ -0,0 +1,52 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class Certificate extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'Certificate'; + } + + static get tableName () { + return 'certificate'; + } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'certificate.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = Certificate; diff --git a/src/backend/routes/api/main.js b/src/backend/routes/api/main.js index c06c243..cbc352e 100644 --- a/src/backend/routes/api/main.js +++ b/src/backend/routes/api/main.js @@ -36,6 +36,7 @@ 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')); +router.use('/nginx/certificates', require('./nginx/certificates')); /** * API 404 for all other routes diff --git a/src/backend/routes/api/nginx/certificates.js b/src/backend/routes/api/nginx/certificates.js new file mode 100644 index 0000000..d419bf4 --- /dev/null +++ b/src/backend/routes/api/nginx/certificates.js @@ -0,0 +1,150 @@ +'use strict'; + +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalCertificate = require('../../../internal/certificate'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/certificates + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/certificates + * + * Retrieve all certificates + */ + .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 internalCertificate.getAll(res.locals.access, data.expand, data.query); + }) + .then(rows => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/certificates + * + * Create a new certificate + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body) + .then(payload => { + return internalCertificate.create(res.locals.access, payload); + }) + .then(result => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific certificate + * + * /api/nginx/certificates/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/certificates/123 + * + * Retrieve a specific certificate + */ + .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 internalCertificate.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/certificates/123 + * + * Update and existing certificate + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body) + .then(payload => { + payload.id = parseInt(req.params.host_id, 10); + return internalCertificate.update(res.locals.access, payload); + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/certificates/123 + * + * Update and existing certificate + */ + .delete((req, res, next) => { + internalCertificate.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/schema/endpoints/users.json b/src/backend/schema/endpoints/users.json index 1202713..42f44ea 100644 --- a/src/backend/schema/endpoints/users.json +++ b/src/backend/schema/endpoints/users.json @@ -243,6 +243,10 @@ "streams": { "type": "string", "pattern": "^(hidden|view|manage)$" + }, + "certificates": { + "type": "string", + "pattern": "^(hidden|view|manage)$" } } }, diff --git a/src/backend/setup.js b/src/backend/setup.js index 8a253c5..5015cbc 100644 --- a/src/backend/setup.js +++ b/src/backend/setup.js @@ -91,7 +91,8 @@ module.exports = function () { redirection_hosts: 'manage', dead_hosts: 'manage', streams: 'manage', - access_lists: 'manage' + access_lists: 'manage', + certificates: 'manage' }); }); }); diff --git a/src/backend/templates/dead_host.conf b/src/backend/templates/dead_host.conf index d136541..102b757 100644 --- a/src/backend/templates/dead_host.conf +++ b/src/backend/templates/dead_host.conf @@ -1,19 +1,23 @@ -# <%- hostname %> +# {{ domain_names | join: ", " }} server { listen 80; - <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + listen 443 ssl; + {%- endif %} + server_name {{ domain_names | join: " " }}; + access_log /data/logs/proxy_host-{{ id }}.log proxy; - server_name <%- hostname %>; - - access_log /config/logs/<%- hostname %>.log proxy; - -<% if (typeof ssl !== 'undefined' && ssl) { -%> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + {%- if ssl_provider == "letsencrypt" %} + # Let's Encrypt SSL + include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; -<% } -%> + ssl_certificate /etc/letsencrypt/live/proxy_host-{{ id }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/proxy_host-{{ id }}/privkey.pem; + {%- endif -%} + {%- endif %} - <%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + # TODO: Advanced config options return 404; } diff --git a/src/backend/templates/letsencrypt.conf b/src/backend/templates/letsencrypt.conf index f870f2e..0434c5b 100644 --- a/src/backend/templates/letsencrypt.conf +++ b/src/backend/templates/letsencrypt.conf @@ -1,11 +1,10 @@ -# Letsencrypt Verification Temporary Host: <%- hostname %> +# Letsencrypt Verification Temporary Host: {{ domain_names | join: ", " }} server { listen 80; - server_name <%- hostname %>; - - access_log /config/logs/letsencrypt.log proxy; + server_name {{ domain_names | join: " " }}; + access_log /data/logs/letsencrypt.log proxy; location / { - root /config/letsencrypt-acme-challenge; + root /data/letsencrypt-acme-challenge; } } diff --git a/src/backend/templates/proxy_host.conf b/src/backend/templates/proxy_host.conf index 4f32036..297ddc1 100644 --- a/src/backend/templates/proxy_host.conf +++ b/src/backend/templates/proxy_host.conf @@ -1,33 +1,51 @@ -# <%- hostname %> +# {{ domain_names | join: ", " }} server { listen 80; - <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + listen 443 ssl; + {%- endif %} + server_name {{ domain_names | join: " " }}; + access_log /data/logs/proxy_host-{{ id }}.log proxy; - server_name <%- hostname %>; + set $server {{ forward_ip }}; + set $port {{ forward_port }}; - access_log /config/logs/<%- hostname %>.log proxy; + {% if caching_enabled == 1 or caching_enabled == true -%} + # Asset Caching + include conf.d/include/assets.conf; + {%- endif %} + {% if block_exploits == 1 or block_exploits == true -%} + # Block Exploits + include conf.d/include/block-exploits.conf; + {%- endif -%} - set $server <%- forward_server %>; - set $port <%- forward_port %>; - - <%- typeof asset_caching !== 'undefined' && asset_caching ? 'include conf.d/include/assets.conf;' : '' %> - <%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %> - -<% if (typeof ssl !== 'undefined' && ssl) { -%> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + {%- if ssl_provider == "letsencrypt" %} + # Let's Encrypt SSL include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; -<% } -%> + ssl_certificate /etc/letsencrypt/live/proxy_host-{{ id }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/proxy_host-{{ id }}/privkey.pem; + {%- endif -%} + {%- endif %} -<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + # TODO: Advanced config options location / { - <% if (typeof access_list_id !== 'undefined' && access_list_id) { -%> + {%- if access_list_id > 0 -%} + # Access List auth_basic "Authorization required"; - auth_basic_user_file /config/access/<%- access_list_id %>; - <% } -%> - <%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %> + auth_basic_user_file /config/access/{{ access_list_id }}; + {%- endif %} + + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + {%- if ssl_forced == 1 or ssl_forced == true -%} + # Force SSL + include conf.d/include/force-ssl.conf; + {%- endif -%} + {%- endif %} + + # Proxy! include conf.d/include/proxy.conf; } } diff --git a/src/backend/templates/redirection_host.conf b/src/backend/templates/redirection_host.conf index 1c4f91b..440be91 100644 --- a/src/backend/templates/redirection_host.conf +++ b/src/backend/templates/redirection_host.conf @@ -1,22 +1,34 @@ -# <%- hostname %> +# {{ domain_names | join: ", " }} server { listen 80; - <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + listen 443 ssl; + {%- endif %} + server_name {{ domain_names | join: " " }}; + access_log /data/logs/proxy_host-{{ id }}.log proxy; - server_name <%- hostname %>; + {%- if caching_enabled == 1 or caching_enabled == true %} + # Asset Caching + include conf.d/include/assets.conf; + {%- endif %} + {%- if block_exploits == 1 or block_exploits == true %} + # Block Exploits + include conf.d/include/block-exploits.conf; + {%- endif -%} - access_log /config/logs/<%- hostname %>.log proxy; - - <%- typeof asset_caching !== 'undefined' && asset_caching ? 'include conf.d/include/assets.conf;' : '' %> - <%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %> - -<% if (typeof ssl !== 'undefined' && ssl) { -%> + {%- if ssl_enabled == 1 or ssl_enabled == true -%} + {%- if ssl_provider == "letsencrypt" %} + # Let's Encrypt SSL + include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; -<% } -%> + ssl_certificate /etc/letsencrypt/live/proxy_host-{{ id }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/proxy_host-{{ id }}/privkey.pem; + {%- endif -%} + {%- endif %} - <%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + # TODO: Advanced config options - return 301 $scheme://<%- forward_host %>$request_uri; + # TODO: Preserve Path Option + + return 301 $scheme://{{ forward_domain_name }}$request_uri; } diff --git a/src/backend/templates/stream.conf b/src/backend/templates/stream.conf index 49994a2..4a0c033 100644 --- a/src/backend/templates/stream.conf +++ b/src/backend/templates/stream.conf @@ -1,11 +1,14 @@ -# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %> -<% -protocols.forEach(function (protocol) { -%> +# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} + +{% if tcp_forwarding == 1 or tcp_forwarding == true -%} server { - listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>; - proxy_pass <%- forward_server %>:<%- forward_port %>; + listen {{ incoming_port }}; + proxy_pass {{ forward_ip }}:{{ forwarding_port }}; } -<% -}); -%> +{% endif %} +{% if udp_forwarding == 1 or udp_forwarding == true %} +server { + listen {{ incoming_port }} udp; + proxy_pass {{ forward_ip }}:{{ forwarding_port }}; +} +{% endif %} diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index 162e358..a45e9c0 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -500,6 +500,43 @@ module.exports = { return fetch('delete', 'nginx/access-lists/' + id); } }, + + Certificates: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/certificates', expand, query); + }, + + /** + * @param {Object} data + */ + create: function (data) { + return fetch('post', 'nginx/certificates', data); + }, + + /** + * @param {Object} data + * @param {Integer} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'nginx/certificates/' + id, data); + }, + + /** + * @param {Integer} id + * @returns {Promise} + */ + delete: function (id) { + return fetch('delete', 'nginx/certificates/' + id); + } + } }, AuditLog: { diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 66ea7e2..11aaaa9 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -319,6 +319,20 @@ module.exports = { } }, + /** + * Nginx Certificates + */ + showNginxCertificates: function () { + if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { + let controller = this; + + require(['./main', './nginx/certificates/main'], (App, View) => { + controller.navigate('/nginx/certificates'); + App.UI.showAppContent(new View()); + }); + } + }, + /** * Audit Log */ diff --git a/src/frontend/js/app/dashboard/main.js b/src/frontend/js/app/dashboard/main.js index 39fe5ea..9d76818 100644 --- a/src/frontend/js/app/dashboard/main.js +++ b/src/frontend/js/app/dashboard/main.js @@ -10,7 +10,7 @@ const template = require('./main.ejs'); module.exports = Mn.View.extend({ template: template, id: 'dashboard', - columns: 0, + columns: 0, stats: {}, @@ -46,7 +46,7 @@ module.exports = Mn.View.extend({ }, columns: view.columns - } + }; }, onRender: function () { diff --git a/src/frontend/js/app/nginx/access/delete.js b/src/frontend/js/app/nginx/access/delete.js index 91be703..e4660cb 100644 --- a/src/frontend/js/app/nginx/access/delete.js +++ b/src/frontend/js/app/nginx/access/delete.js @@ -4,8 +4,6 @@ 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', @@ -22,7 +20,7 @@ module.exports = Mn.View.extend({ 'click @ui.save': function (e) { e.preventDefault(); - App.Api.Nginx.ProxyHosts.delete(this.model.get('id')) + App.Api.Nginx.AccessLists.delete(this.model.get('id')) .then(() => { App.Controller.showNginxAccess(); App.UI.closeModal(); diff --git a/src/frontend/js/app/nginx/certificates/delete.ejs b/src/frontend/js/app/nginx/certificates/delete.ejs new file mode 100644 index 0000000..b4e0686 --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/delete.ejs @@ -0,0 +1,19 @@ + diff --git a/src/frontend/js/app/nginx/certificates/delete.js b/src/frontend/js/app/nginx/certificates/delete.js new file mode 100644 index 0000000..2499bad --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/delete.js @@ -0,0 +1,34 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./delete.ejs'); + +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.Certificates.delete(this.model.get('id')) + .then(() => { + App.Controller.showNginxCertificates(); + 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/certificates/form.ejs b/src/frontend/js/app/nginx/certificates/form.ejs new file mode 100644 index 0000000..f1cfc6a --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/form.ejs @@ -0,0 +1,122 @@ + diff --git a/src/frontend/js/app/nginx/certificates/form.js b/src/frontend/js/app/nginx/certificates/form.js new file mode 100644 index 0000000..4a4966e --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/form.js @@ -0,0 +1,195 @@ +'use strict'; + +const _ = require('underscore'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const CertificateModel = require('../../../models/certificate'); +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 CertificateModel.Model(); + } + } +}); diff --git a/src/frontend/js/app/nginx/certificates/list/item.ejs b/src/frontend/js/app/nginx/certificates/list/item.ejs new file mode 100644 index 0000000..7a815a0 --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/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/certificates/list/item.js b/src/frontend/js/app/nginx/certificates/list/item.js new file mode 100644 index 0000000..e289bec --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/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.showNginxCertificateForm(this.model); + }, + + 'click @ui.delete': function (e) { + e.preventDefault(); + App.Controller.showNginxCertificateDeleteConfirm(this.model); + } + }, + + templateContext: { + canManage: App.Cache.User.canManage('certificates') + }, + + initialize: function () { + this.listenTo(this.model, 'change', this.render); + } +}); diff --git a/src/frontend/js/app/nginx/certificates/list/main.ejs b/src/frontend/js/app/nginx/certificates/list/main.ejs new file mode 100644 index 0000000..f2c64ea --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/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/certificates/list/main.js b/src/frontend/js/app/nginx/certificates/list/main.js new file mode 100644 index 0000000..6472604 --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/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('certificates') + }, + + onRender: function () { + this.showChildView('body', new TableBody({ + collection: this.collection + })); + } +}); diff --git a/src/frontend/js/app/nginx/certificates/main.ejs b/src/frontend/js/app/nginx/certificates/main.ejs new file mode 100644 index 0000000..19b88be --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/main.ejs @@ -0,0 +1,20 @@ +
+
+
+

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

+
+ + <% if (showAddButton) { %> + <%- i18n('certificates', 'add') %> + <% } %> +
+
+
+
+
+
+ +
+
+
+
diff --git a/src/frontend/js/app/nginx/certificates/main.js b/src/frontend/js/app/nginx/certificates/main.js new file mode 100644 index 0000000..4572930 --- /dev/null +++ b/src/frontend/js/app/nginx/certificates/main.js @@ -0,0 +1,83 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const CertificateModel = require('../../../models/certificate'); +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-certificates', + template: template, + + ui: { + list_region: '.list-region', + add: '.add-item', + help: '.help', + dimmer: '.dimmer' + }, + + regions: { + list_region: '@ui.list_region' + }, + + events: { + 'click @ui.add': function (e) { + e.preventDefault(); + App.Controller.showNginxCertificateForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('certificates', 'help-title'), App.i18n('certificates', 'help-content')); + } + }, + + templateContext: { + showAddButton: App.Cache.User.canManage('certificates') + }, + + onRender: function () { + let view = this; + + App.Api.Nginx.Certificates.getAll(['owner']) + .then(response => { + if (!view.isDestroyed()) { + if (response && response.length) { + view.showChildView('list_region', new ListView({ + collection: new CertificateModel.Collection(response) + })); + } else { + let manage = App.Cache.User.canManage('certificates'); + + view.showChildView('list_region', new EmptyView({ + title: App.i18n('certificates', 'empty'), + subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), + link: manage ? App.i18n('certificates', 'add') : null, + btn_color: 'teal', + permission: 'certificates', + action: function () { + App.Controller.showNginxCertificateForm(); + } + })); + } + } + }) + .catch(err => { + view.showChildView('list_region', new ErrorView({ + code: err.code, + message: err.message, + retry: function () { + App.Controller.showNginxCertificates(); + } + })); + + console.error(err); + }) + .then(() => { + view.ui.dimmer.removeClass('active'); + }); + } +}); diff --git a/src/frontend/js/app/nginx/dead/delete.js b/src/frontend/js/app/nginx/dead/delete.js index 0ae2486..81356f3 100644 --- a/src/frontend/js/app/nginx/dead/delete.js +++ b/src/frontend/js/app/nginx/dead/delete.js @@ -4,8 +4,6 @@ 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', diff --git a/src/frontend/js/app/nginx/dead/list/item.ejs b/src/frontend/js/app/nginx/dead/list/item.ejs index b8f3c77..0131bb1 100644 --- a/src/frontend/js/app/nginx/dead/list/item.ejs +++ b/src/frontend/js/app/nginx/dead/list/item.ejs @@ -19,6 +19,17 @@
<%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %>
+ + <% + var o = isOnline(); + if (o === true) { %> + <%- i18n('str', 'online') %> + <% } else if (o === false) { %> + <%- i18n('str', 'offline') %> + <% } else { %> + <%- i18n('str', 'unknown') %> + <% } %> + <% if (canManage) { %>