From 66e25e315be0355bc3c1c130fb651dc83f33a472 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 1 Aug 2018 21:18:17 +1000 Subject: [PATCH] Audit Log items, backend stuff, help pages --- rootfs/etc/nginx/nginx.conf | 4 +- rootfs/etc/services.d/nginx/run | 2 +- src/backend/internal/audit-log.js | 57 ++++-- src/backend/internal/dead-host.js | 53 +++++- src/backend/internal/nginx.js | 92 ++++++++++ src/backend/internal/proxy-host.js | 53 +++++- src/backend/internal/redirection-host.js | 45 ++++- src/backend/internal/ssl.js | 163 ++++++++++++++++++ src/backend/internal/stream.js | 38 +++- src/backend/internal/user.js | 62 ++++++- src/backend/lib/error.js | 9 + src/backend/lib/utils.js | 22 +++ src/backend/logger.js | 10 +- .../migrations/20180618015850_initial.js | 3 +- src/backend/models/audit-log.js | 21 +++ src/backend/templates/dead_host.conf | 19 ++ src/backend/templates/letsencrypt.conf | 11 ++ src/backend/templates/proxy_host.conf | 33 ++++ src/backend/templates/redirection_host.conf | 22 +++ src/backend/templates/stream.conf | 11 ++ src/frontend/js/app/audit-log/list/item.ejs | 88 +++++++--- src/frontend/js/app/audit-log/list/item.js | 62 ++----- src/frontend/js/app/audit-log/list/main.ejs | 5 +- src/frontend/js/app/audit-log/main.js | 2 +- src/frontend/js/app/audit-log/meta.ejs | 27 +++ src/frontend/js/app/audit-log/meta.js | 9 + src/frontend/js/app/controller.js | 25 +++ src/frontend/js/app/help/main.ejs | 12 ++ src/frontend/js/app/help/main.js | 18 ++ src/frontend/js/app/nginx/access/main.ejs | 1 + src/frontend/js/app/nginx/access/main.js | 6 + src/frontend/js/app/nginx/dead/list/item.ejs | 2 +- src/frontend/js/app/nginx/dead/main.ejs | 1 + src/frontend/js/app/nginx/dead/main.js | 6 + src/frontend/js/app/nginx/proxy/list/item.ejs | 2 +- src/frontend/js/app/nginx/proxy/main.ejs | 1 + src/frontend/js/app/nginx/proxy/main.js | 6 + .../js/app/nginx/redirection/list/item.ejs | 2 +- .../js/app/nginx/redirection/main.ejs | 1 + src/frontend/js/app/nginx/redirection/main.js | 6 + src/frontend/js/app/nginx/stream/main.ejs | 1 + src/frontend/js/app/nginx/stream/main.js | 6 + src/frontend/js/app/users/list/item.ejs | 2 +- src/frontend/js/app/users/main.ejs | 2 +- src/frontend/js/i18n/messages.json | 37 +++- src/frontend/js/lib/marionette.js | 1 + src/frontend/scss/tabler-extra.scss | 9 + 47 files changed, 936 insertions(+), 134 deletions(-) create mode 100644 src/backend/internal/nginx.js create mode 100644 src/backend/internal/ssl.js create mode 100644 src/backend/lib/utils.js create mode 100644 src/backend/templates/dead_host.conf create mode 100644 src/backend/templates/letsencrypt.conf create mode 100644 src/backend/templates/proxy_host.conf create mode 100644 src/backend/templates/redirection_host.conf create mode 100644 src/backend/templates/stream.conf create mode 100644 src/frontend/js/app/audit-log/meta.ejs create mode 100644 src/frontend/js/app/audit-log/meta.js create mode 100644 src/frontend/js/app/help/main.ejs create mode 100644 src/frontend/js/app/help/main.js diff --git a/rootfs/etc/nginx/nginx.conf b/rootfs/etc/nginx/nginx.conf index fb44624..8dddd30 100644 --- a/rootfs/etc/nginx/nginx.conf +++ b/rootfs/etc/nginx/nginx.conf @@ -51,7 +51,9 @@ http { access_log /data/logs/default.log proxy; include /etc/nginx/conf.d/*.conf; - include /data/nginx/*.conf; + include /data/nginx/proxy_host/*.conf; + include /data/nginx/redirection_host/*.conf; + include /data/nginx/dead_host/*.conf; } stream { diff --git a/rootfs/etc/services.d/nginx/run b/rootfs/etc/services.d/nginx/run index 0ae1102..7bf1449 100755 --- a/rootfs/etc/services.d/nginx/run +++ b/rootfs/etc/services.d/nginx/run @@ -2,7 +2,7 @@ mkdir -p /tmp/nginx \ /data/{nginx,logs,access} \ - /data/nginx/stream \ + /data/nginx/{proxy_host,redirection_host,stream,dead_host} \ /var/lib/nginx/cache/{public,private} chown root /tmp/nginx diff --git a/src/backend/internal/audit-log.js b/src/backend/internal/audit-log.js index cb4b8c1..926773e 100644 --- a/src/backend/internal/audit-log.js +++ b/src/backend/internal/audit-log.js @@ -1,19 +1,10 @@ 'use strict'; +const error = require('../lib/error'); const auditLogModel = require('../models/audit-log'); const internalAuditLog = { - /** - * Internal use only - * - * @param {Object} data - * @returns {Promise} - */ - create: data => { - // TODO - }, - /** * All logs * @@ -28,16 +19,14 @@ const internalAuditLog = { let query = auditLogModel .query() .orderBy('created_on', 'DESC') - .limit(100); + .limit(100) + .allowEager('[user]'); // Query is used for searching if (typeof search_query === 'string') { - /* query.where(function () { - this.where('name', 'like', '%' + search_query + '%') - .orWhere('email', 'like', '%' + search_query + '%'); + this.where('meta', 'like', '%' + search_query + '%'); }); - */ } if (typeof expand !== 'undefined' && expand !== null) { @@ -46,6 +35,44 @@ const internalAuditLog = { return query; }); + }, + + /** + * This method should not be publicly used, it doesn't check certain things. It will be assumed + * that permission to add to audit log is already considered, however the access token is used for + * default user id determination. + * + * @param {Access} access + * @param {Object} data + * @param {String} data.action + * @param {Integer} [data.user_id] + * @param {Integer} [data.object_id] + * @param {Integer} [data.object_type] + * @param {Object} [data.meta] + * @returns {Promise} + */ + add: (access, data) => { + return new Promise((resolve, reject) => { + // Default the user id + if (typeof data.user_id === 'undefined' || !data.user_id) { + data.user_id = access.token.get('attrs').id; + } + + if (typeof data.action === 'undefined' || !data.action) { + reject(new error.InternalValidationError('Audit log entry must contain an Action')); + } else { + // Make sure at least 1 of the IDs are set and action + resolve(auditLogModel + .query() + .insert({ + user_id: data.user_id, + action: data.action, + object_type: data.object_type || '', + object_id: data.object_id || 0, + meta: data.meta || {} + })); + } + }); } }; diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index cadbc73..8630f8d 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -1,9 +1,10 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const deadHostModel = require('../models/dead_host'); -const internalHost = require('./host'); +const _ = require('lodash'); +const error = require('../lib/error'); +const deadHostModel = require('../models/dead_host'); +const internalHost = require('./host'); +const internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -49,7 +50,16 @@ const internalDeadHost = { .insertAndFetch(data); }) .then(row => { - return _.omit(row, omissions()); + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'dead-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(row, omissions()); + }); }); }, @@ -97,7 +107,17 @@ const internalDeadHost = { .patchAndFetchById(row.id, data) .then(saved_row => { saved_row.meta = internalHost.cleanMeta(saved_row.meta); - return _.omit(saved_row, omissions()); + + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'dead-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); }); }); }, @@ -171,6 +191,17 @@ const internalDeadHost = { .where('id', row.id) .patch({ is_deleted: 1 + }) + .then(() => { + // Add to audit log + row.meta = internalHost.cleanMeta(row.meta); + + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'dead-host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); }); }) .then(() => { @@ -200,7 +231,15 @@ const internalDeadHost = { }); }) .then(row => { - return _.pick(row.meta, internalHost.allowed_ssl_files); + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'dead-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); }); }, diff --git a/src/backend/internal/nginx.js b/src/backend/internal/nginx.js new file mode 100644 index 0000000..de163ff --- /dev/null +++ b/src/backend/internal/nginx.js @@ -0,0 +1,92 @@ +'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 internalNginx = { + + /** + * @returns {Promise} + */ + test: () => { + logger.info('Testing Nginx configuration'); + return utils.exec('/usr/sbin/nginx -t'); + }, + + /** + * @returns {Promise} + */ + reload: () => { + return internalNginx.test() + .then(() => { + logger.info('Reloading Nginx'); + return utils.exec('/usr/sbin/nginx -s reload'); + }); + }, + + /** + * @param {String} host_type + * @param {Integer} host_id + * @returns {String} + */ + getConfigName: (host_type, host_id) => { + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + return '/data/nginx/' + host_type + '/' + host_id + '.conf'; + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + generateConfig: (host_type, host) => { + let renderEngine = Liquid(); + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + + return new Promise((resolve, reject) => { + let template = null; + let filename = internalNginx.getConfigName(host_type, host.id); + try { + template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); + } catch (err) { + reject(new error.ConfigurationError(err.message)); + return; + } + + return renderEngine + .parseAndRender(template, host) + .then(config_text => { + fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + return true; + }) + .catch(err => { + throw new error.ConfigurationError(err.message); + }); + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @param {Boolean} [throw_errors] + * @returns {Promise} + */ + deleteConfig: (host_type, host, throw_errors) => { + return new Promise((resolve, reject) => { + try { + fs.unlinkSync(internalNginx.getConfigName(host_type, host.id)); + } catch (err) { + if (throw_errors) { + reject(err); + } + } + + resolve(); + }); + } +}; + +module.exports = internalNginx; diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index ded58d9..391d6ac 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -1,9 +1,10 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const proxyHostModel = require('../models/proxy_host'); -const internalHost = require('./host'); +const _ = require('lodash'); +const error = require('../lib/error'); +const proxyHostModel = require('../models/proxy_host'); +const internalHost = require('./host'); +const internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -49,7 +50,16 @@ const internalProxyHost = { .insertAndFetch(data); }) .then(row => { - return _.omit(row, omissions()); + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'proxy-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(row, omissions()); + }); }); }, @@ -97,7 +107,17 @@ const internalProxyHost = { .patchAndFetchById(row.id, data) .then(saved_row => { saved_row.meta = internalHost.cleanMeta(saved_row.meta); - return _.omit(saved_row, omissions()); + + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'proxy-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); }); }); }, @@ -171,6 +191,17 @@ const internalProxyHost = { .where('id', row.id) .patch({ is_deleted: 1 + }) + .then(() => { + // Add to audit log + row.meta = internalHost.cleanMeta(row.meta); + + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'proxy-host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); }); }) .then(() => { @@ -200,7 +231,15 @@ const internalProxyHost = { }); }) .then(row => { - return _.pick(row.meta, internalHost.allowed_ssl_files); + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'proxy-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); }); }, diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index 7f0d711..32ecd64 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 internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -49,7 +50,16 @@ const internalRedirectionHost = { .insertAndFetch(data); }) .then(row => { - return _.omit(row, omissions()); + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'redirection-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(row, omissions()); + }); }); }, @@ -97,7 +107,17 @@ const internalRedirectionHost = { .patchAndFetchById(row.id, data) .then(saved_row => { saved_row.meta = internalHost.cleanMeta(saved_row.meta); - return _.omit(saved_row, omissions()); + + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'redirection-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); }); }); }, @@ -171,6 +191,17 @@ const internalRedirectionHost = { .where('id', row.id) .patch({ is_deleted: 1 + }) + .then(() => { + // Add to audit log + row.meta = internalHost.cleanMeta(row.meta); + + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'redirection-host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); }); }) .then(() => { @@ -200,7 +231,15 @@ const internalRedirectionHost = { }); }) .then(row => { - return _.pick(row.meta, internalHost.allowed_ssl_files); + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'redirection-host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); }); }, diff --git a/src/backend/internal/ssl.js b/src/backend/internal/ssl.js new file mode 100644 index 0000000..efa0930 --- /dev/null +++ b/src/backend/internal/ssl.js @@ -0,0 +1,163 @@ +'use strict'; + +const fs = require('fs'); +const Liquid = require('liquidjs'); +const timestamp = require('unix-timestamp'); +const internalNginx = require('./nginx'); +const logger = require('../logger').ssl; +const utils = require('../lib/utils'); +const error = require('../lib/error'); + +timestamp.round = true; + +const internalSsl = { + + interval_timeout: 1000 * 60 * 60 * 12, // 12 hours + interval: null, + interval_processing: false, + + initTimer: () => { + internalSsl.interval = setInterval(internalSsl.processExpiringHosts, internalSsl.interval_timeout); + }, + + /** + * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required + */ + processExpiringHosts: () => { + if (!internalSsl.interval_processing) { + logger.info('Renewing SSL certs close to expiry...'); + return utils.exec('/usr/bin/certbot renew -q') + .then(result => { + logger.info(result); + internalSsl.interval_processing = false; + + return internalNginx.reload() + .then(() => { + logger.info('Renew Complete'); + return result; + }); + }) + .catch(err => { + logger.error(err); + internalSsl.interval_processing = false; + }); + } + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Boolean} + */ + hasValidSslCerts: (host_type, host) => { + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + let le_path = '/etc/letsencrypt/live/' + host_type + '_' + host.id; + + return fs.existsSync(le_path + '/fullchain.pem') && fs.existsSync(le_path + '/privkey.pem'); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + requestSsl: (host_type, host) => { + logger.info('Requesting SSL certificates for ' + host_type + ' #' + host.id); + + // TODO + + return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot -d "' + host.hostname + '"') + .then(result => { + logger.info(result); + return result; + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + renewSsl: (host_type, host) => { + logger.info('Renewing SSL certificates for ' + host_type + ' #' + host.id); + + // TODO + + return utils.exec('/usr/bin/certbot renew --force-renewal --disable-hook-validation --cert-name "' + host.hostname + '"') + .then(result => { + logger.info(result); + return result; + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + deleteCerts: (host_type, host) => { + logger.info('Deleting SSL certificates for ' + host_type + ' #' + host.id); + + // TODO + + return utils.exec('/usr/bin/certbot delete -n --cert-name "' + host.hostname + '"') + .then(result => { + logger.info(result); + }) + .catch(err => { + logger.error(err); + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + generateSslSetupConfig: (host_type, host) => { + host_type = host_type.replace(new RegExp('-', 'g'), '_'); + + let renderEngine = Liquid(); + let template = null; + let filename = internalNginx.getConfigName(host_type, host); + + return new Promise((resolve, reject) => { + try { + template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf', {encoding: 'utf8'}); + } catch (err) { + reject(new error.ConfigurationError(err.message)); + return; + } + + return renderEngine + .parseAndRender(template, host) + .then(config_text => { + fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + return template_data; + }) + .catch(err => { + throw new error.ConfigurationError(err.message); + }); + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + configureSsl: (host_type, host) => { + + // TODO + + return internalSsl.generateSslSetupConfig(host) + .then(data => { + return internalNginx.reload() + .then(() => { + return internalSsl.requestSsl(data); + }); + }); + } +}; + +module.exports = internalSsl; diff --git a/src/backend/internal/stream.js b/src/backend/internal/stream.js index 603b8fd..bb8c9c3 100644 --- a/src/backend/internal/stream.js +++ b/src/backend/internal/stream.js @@ -1,8 +1,9 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const streamModel = require('../models/stream'); +const _ = require('lodash'); +const error = require('../lib/error'); +const streamModel = require('../models/stream'); +const internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -31,7 +32,16 @@ const internalStream = { .insertAndFetch(data); }) .then(row => { - return _.omit(row, omissions()); + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'stream', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(row, omissions()); + }); }); }, @@ -60,7 +70,16 @@ const internalStream = { .omit(omissions()) .patchAndFetchById(row.id, data) .then(saved_row => { - return _.omit(saved_row, omissions()); + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'stream', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); }); }); }, @@ -133,6 +152,15 @@ const internalStream = { .where('id', row.id) .patch({ is_deleted: 1 + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'stream', + object_id: row.id, + meta: _.omit(row, omissions()) + }); }); }) .then(() => { diff --git a/src/backend/internal/user.js b/src/backend/internal/user.js index 9cbf63d..6a91e98 100644 --- a/src/backend/internal/user.js +++ b/src/backend/internal/user.js @@ -7,6 +7,7 @@ const userPermissionModel = require('../models/user_permission'); const authModel = require('../models/auth'); const gravatar = require('gravatar'); const internalToken = require('./token'); +const internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -74,6 +75,18 @@ const internalUser = { .then(() => { return internalUser.get(access, {id: user.id, expand: ['permissions']}); }); + }) + .then(user => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'user', + object_id: user.id, + meta: user + }) + .then(() => { + return user; + }); }); }, @@ -136,6 +149,18 @@ const internalUser = { }) .then(() => { return internalUser.get(access, {id: data.id}); + }) + .then(user => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'user', + object_id: user.id, + meta: data + }) + .then(() => { + return user; + }); }); }, @@ -236,6 +261,15 @@ const internalUser = { .where('id', user.id) .patch({ is_deleted: 1 + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'user', + object_id: user.id, + meta: _.omit(user, omissions()) + }); }); }) .then(() => { @@ -389,6 +423,19 @@ const internalUser = { meta: {} }); } + }) + .then(() => { + // Add to Audit Log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'user', + object_id: user.id, + meta: { + name: user.name, + password_changed: true, + auth_type: data.type + } + }); }); }) .then(() => { @@ -435,8 +482,21 @@ const internalUser = { } }) .then(permissions => { - return true; + // Add to Audit Log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'user', + object_id: user.id, + meta: { + name: user.name, + permissions: permissions + } + }); + }); + }) + .then(() => { + return true; }); }, diff --git a/src/backend/lib/error.js b/src/backend/lib/error.js index 1580c45..070952f 100644 --- a/src/backend/lib/error.js +++ b/src/backend/lib/error.js @@ -50,6 +50,15 @@ module.exports = { this.public = false; }, + ConfigurationError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 400; + this.public = true; + }, + CacheError: function (message, previous) { Error.captureStackTrace(this, this.constructor); this.name = this.constructor.name; diff --git a/src/backend/lib/utils.js b/src/backend/lib/utils.js new file mode 100644 index 0000000..28919b1 --- /dev/null +++ b/src/backend/lib/utils.js @@ -0,0 +1,22 @@ +'use strict'; + +const exec = require('child_process').exec; + +module.exports = { + + /** + * @param {String} cmd + * @returns {Promise} + */ + exec: function (cmd) { + return new Promise((resolve, reject) => { + exec(cmd, function (err, stdout, stderr) { + if (err && typeof err === 'object') { + reject(err); + } else { + resolve(stdout.trim()); + } + }); + }); + } +}; diff --git a/src/backend/logger.js b/src/backend/logger.js index aeb4c70..584287b 100644 --- a/src/backend/logger.js +++ b/src/backend/logger.js @@ -1,8 +1,10 @@ const {Signale} = require('signale'); module.exports = { - global: new Signale({scope: 'Global '}), - migrate: new Signale({scope: 'Migrate '}), - express: new Signale({scope: 'Express '}), - access: new Signale({scope: 'Access '}) + global: new Signale({scope: 'Global '}), + migrate: new Signale({scope: 'Migrate '}), + express: new Signale({scope: 'Express '}), + access: new Signale({scope: 'Access '}), + nginx: new Signale({scope: 'Nginx '}), + ssl: new Signale({scope: 'SSL '}) }; diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 3ce8d01..153f353 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -165,7 +165,8 @@ exports.up = function (knex/*, Promise*/) { table.dateTime('created_on').notNull(); table.dateTime('modified_on').notNull(); table.integer('user_id').notNull().unsigned(); - // TODO + table.string('object_type').notNull().defaultTo(''); + table.integer('object_id').notNull().unsigned().defaultTo(0); table.string('action').notNull(); table.json('meta').notNull(); }); diff --git a/src/backend/models/audit-log.js b/src/backend/models/audit-log.js index e60273c..d9f312e 100644 --- a/src/backend/models/audit-log.js +++ b/src/backend/models/audit-log.js @@ -5,6 +5,7 @@ const db = require('../db'); const Model = require('objection').Model; +const User = require('./user'); Model.knex(db); @@ -25,6 +26,26 @@ class AuditLog extends Model { static get tableName () { return 'audit_log'; } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + user: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'audit_log.user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.omit(['id', 'created_on', 'modified_on', 'roles']); + } + } + }; + } } module.exports = AuditLog; diff --git a/src/backend/templates/dead_host.conf b/src/backend/templates/dead_host.conf new file mode 100644 index 0000000..d136541 --- /dev/null +++ b/src/backend/templates/dead_host.conf @@ -0,0 +1,19 @@ +# <%- hostname %> +server { + listen 80; + <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + + server_name <%- hostname %>; + + access_log /config/logs/<%- hostname %>.log proxy; + +<% if (typeof ssl !== 'undefined' && ssl) { -%> + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; +<% } -%> + + <%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + + return 404; +} diff --git a/src/backend/templates/letsencrypt.conf b/src/backend/templates/letsencrypt.conf new file mode 100644 index 0000000..f870f2e --- /dev/null +++ b/src/backend/templates/letsencrypt.conf @@ -0,0 +1,11 @@ +# Letsencrypt Verification Temporary Host: <%- hostname %> +server { + listen 80; + server_name <%- hostname %>; + + access_log /config/logs/letsencrypt.log proxy; + + location / { + root /config/letsencrypt-acme-challenge; + } +} diff --git a/src/backend/templates/proxy_host.conf b/src/backend/templates/proxy_host.conf new file mode 100644 index 0000000..4f32036 --- /dev/null +++ b/src/backend/templates/proxy_host.conf @@ -0,0 +1,33 @@ +# <%- hostname %> +server { + listen 80; + <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + + server_name <%- hostname %>; + + access_log /config/logs/<%- hostname %>.log proxy; + + 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) { -%> + 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; +<% } -%> + +<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + + location / { + <% if (typeof access_list_id !== 'undefined' && access_list_id) { -%> + 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;' : '' %> + include conf.d/include/proxy.conf; + } +} diff --git a/src/backend/templates/redirection_host.conf b/src/backend/templates/redirection_host.conf new file mode 100644 index 0000000..1c4f91b --- /dev/null +++ b/src/backend/templates/redirection_host.conf @@ -0,0 +1,22 @@ +# <%- hostname %> +server { + listen 80; + <%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %> + + server_name <%- hostname %>; + + 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) { -%> + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem; +<% } -%> + + <%- typeof advanced !== 'undefined' && advanced ? advanced : '' %> + + return 301 $scheme://<%- forward_host %>$request_uri; +} diff --git a/src/backend/templates/stream.conf b/src/backend/templates/stream.conf new file mode 100644 index 0000000..49994a2 --- /dev/null +++ b/src/backend/templates/stream.conf @@ -0,0 +1,11 @@ +# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %> +<% +protocols.forEach(function (protocol) { +%> +server { + listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>; + proxy_pass <%- forward_server %>:<%- forward_port %>; +} +<% +}); +%> diff --git a/src/frontend/js/app/audit-log/list/item.ejs b/src/frontend/js/app/audit-log/list/item.ejs index bd4d19e..499b807 100644 --- a/src/frontend/js/app/audit-log/list/item.ejs +++ b/src/frontend/js/app/audit-log/list/item.ejs @@ -1,32 +1,72 @@ -
- +
+
-
<%- name %>
+
+ <% if (user.is_deleted) { + %> + <%- user.name %> + <% + } else { + %> + <%- user.name %> + <% + } + %> +
+ + +
+ <% + var items = []; + switch (object_type) { + case 'proxy-host': + %> <% + items = meta.domain_names; + break; + case 'redirection-host': + %> <% + items = meta.domain_names; + break; + case 'stream': + %> <% + items.push(meta.incoming_port); + break; + case 'dead-host': + %> <% + items = meta.domain_names; + break; + case 'access-list': + %> <% + items.push(meta.name); + break; + case 'user': + %> <% + items.push(meta.name); + break; + } + %> <%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %> + — + <% + if (items && items.length) { + items.map(function(item) { + %> + <%- item %> + <% + }); + } else { + %> + #<%- object_id %> + <% + } + %> +
- Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %> + <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %>
- -
<%- email %>
- - -
<%- roles.join(', ') %>
- - - + + <%- i18n('audit-log', 'view-meta') %> diff --git a/src/frontend/js/app/audit-log/list/item.js b/src/frontend/js/app/audit-log/list/item.js index 6766f08..f154931 100644 --- a/src/frontend/js/app/audit-log/list/item.js +++ b/src/frontend/js/app/audit-log/list/item.js @@ -2,9 +2,6 @@ const Mn = require('backbone.marionette'); const Controller = require('../../controller'); -const Api = require('../../api'); -const Cache = require('../../cache'); -const Tokens = require('../../tokens'); const template = require('./item.ejs'); module.exports = Mn.View.extend({ @@ -12,61 +9,26 @@ module.exports = Mn.View.extend({ tagName: 'tr', ui: { - edit: 'a.edit-user', - permissions: 'a.edit-permissions', - password: 'a.set-password', - login: 'a.login', - delete: 'a.delete-user' + meta: 'a.meta' }, events: { - 'click @ui.edit': function (e) { + 'click @ui.meta': function (e) { e.preventDefault(); - Controller.showUserForm(this.model); - }, - - 'click @ui.permissions': function (e) { - e.preventDefault(); - Controller.showUserPermissions(this.model); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - Controller.showUserPasswordForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - Controller.showUserDeleteConfirm(this.model); - }, - - 'click @ui.login': function (e) { - e.preventDefault(); - - if (Cache.User.get('id') !== this.model.get('id')) { - this.ui.login.prop('disabled', true).addClass('btn-disabled'); - - Api.Users.loginAs(this.model.get('id')) - .then(res => { - Tokens.addToken(res.token, res.user.nickname || res.user.name); - window.location = '/'; - window.location.reload(); - }) - .catch(err => { - alert(err.message); - this.ui.login.prop('disabled', false).removeClass('btn-disabled'); - }); - } + Controller.showAuditMeta(this.model); } }, templateContext: { - isSelf: function () { - return Cache.User.get('id') === this.id; - } - }, + more: function() { + switch (this.object_type) { + case 'redirection-host': + case 'stream': + case 'proxy-host': + return this.meta.domain_names.join(', '); + } - initialize: function () { - this.listenTo(this.model, 'change', this.render); + return '#' + (this.object_id || '?'); + } } }); diff --git a/src/frontend/js/app/audit-log/list/main.ejs b/src/frontend/js/app/audit-log/list/main.ejs index ce89341..ec3cf2a 100644 --- a/src/frontend/js/app/audit-log/list/main.ejs +++ b/src/frontend/js/app/audit-log/list/main.ejs @@ -1,8 +1,7 @@   - Name - Email - Roles + User + Event   diff --git a/src/frontend/js/app/audit-log/main.js b/src/frontend/js/app/audit-log/main.js index 69a30a2..b9c2f15 100644 --- a/src/frontend/js/app/audit-log/main.js +++ b/src/frontend/js/app/audit-log/main.js @@ -24,7 +24,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - App.Api.AuditLog.getAll() + App.Api.AuditLog.getAll(['user']) .then(response => { if (!view.isDestroyed() && response && response.length) { view.showChildView('list_region', new ListView({ diff --git a/src/frontend/js/app/audit-log/meta.ejs b/src/frontend/js/app/audit-log/meta.ejs new file mode 100644 index 0000000..ce08b90 --- /dev/null +++ b/src/frontend/js/app/audit-log/meta.ejs @@ -0,0 +1,27 @@ + diff --git a/src/frontend/js/app/audit-log/meta.js b/src/frontend/js/app/audit-log/meta.js new file mode 100644 index 0000000..9ec962b --- /dev/null +++ b/src/frontend/js/app/audit-log/meta.js @@ -0,0 +1,9 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./meta.ejs'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog wide' +}); diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 7e80684..66ea7e2 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -280,6 +280,18 @@ module.exports = { } }, + /** + * Help Dialog + * + * @param {String} title + * @param {String} content + */ + showHelp: function (title, content) { + require(['./main', './help/main'], function (App, View) { + App.UI.showModalDialog(new View({title: title, content: content})); + }); + }, + /** * Nginx Access */ @@ -322,6 +334,19 @@ module.exports = { } }, + /** + * Audit Log Metadata + * + * @param model + */ + showAuditMeta: function (model) { + if (Cache.User.isAdmin()) { + require(['./main', './audit-log/meta'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Logout */ diff --git a/src/frontend/js/app/help/main.ejs b/src/frontend/js/app/help/main.ejs new file mode 100644 index 0000000..6fb79e6 --- /dev/null +++ b/src/frontend/js/app/help/main.ejs @@ -0,0 +1,12 @@ + diff --git a/src/frontend/js/app/help/main.js b/src/frontend/js/app/help/main.js new file mode 100644 index 0000000..c578777 --- /dev/null +++ b/src/frontend/js/app/help/main.js @@ -0,0 +1,18 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./main.ejs'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog wide', + + templateContext: function () { + let content = this.getOption('content').split("\n"); + + return { + title: this.getOption('title'), + content: '

' + content.join('

') + '

' + }; + } +}); diff --git a/src/frontend/js/app/nginx/access/main.ejs b/src/frontend/js/app/nginx/access/main.ejs index 140cd49..c245ff4 100644 --- a/src/frontend/js/app/nginx/access/main.ejs +++ b/src/frontend/js/app/nginx/access/main.ejs @@ -3,6 +3,7 @@

<%- 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 0214bd7..dc31496 100644 --- a/src/frontend/js/app/nginx/access/main.js +++ b/src/frontend/js/app/nginx/access/main.js @@ -15,6 +15,7 @@ module.exports = Mn.View.extend({ ui: { list_region: '.list-region', add: '.add-item', + help: '.help', dimmer: '.dimmer' }, @@ -26,6 +27,11 @@ module.exports = Mn.View.extend({ 'click @ui.add': function (e) { e.preventDefault(); App.Controller.showNginxAccessListForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('access-lists', 'help-title'), App.i18n('access-lists', 'help-content')); } }, diff --git a/src/frontend/js/app/nginx/dead/list/item.ejs b/src/frontend/js/app/nginx/dead/list/item.ejs index 71b3cda..b8f3c77 100644 --- a/src/frontend/js/app/nginx/dead/list/item.ejs +++ b/src/frontend/js/app/nginx/dead/list/item.ejs @@ -25,7 +25,7 @@ diff --git a/src/frontend/js/app/nginx/dead/main.ejs b/src/frontend/js/app/nginx/dead/main.ejs index 9951fb7..508280a 100644 --- a/src/frontend/js/app/nginx/dead/main.ejs +++ b/src/frontend/js/app/nginx/dead/main.ejs @@ -3,6 +3,7 @@

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

+ <% if (showAddButton) { %> <%- i18n('dead-hosts', 'add') %> <% } %> diff --git a/src/frontend/js/app/nginx/dead/main.js b/src/frontend/js/app/nginx/dead/main.js index 151b807..e8e6a22 100644 --- a/src/frontend/js/app/nginx/dead/main.js +++ b/src/frontend/js/app/nginx/dead/main.js @@ -15,6 +15,7 @@ module.exports = Mn.View.extend({ ui: { list_region: '.list-region', add: '.add-item', + help: '.help', dimmer: '.dimmer' }, @@ -26,6 +27,11 @@ module.exports = Mn.View.extend({ 'click @ui.add': function (e) { e.preventDefault(); App.Controller.showNginxDeadForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('dead-hosts', 'help-title'), App.i18n('dead-hosts', 'help-content')); } }, diff --git a/src/frontend/js/app/nginx/proxy/list/item.ejs b/src/frontend/js/app/nginx/proxy/list/item.ejs index b1aab6e..9621df9 100644 --- a/src/frontend/js/app/nginx/proxy/list/item.ejs +++ b/src/frontend/js/app/nginx/proxy/list/item.ejs @@ -31,7 +31,7 @@ diff --git a/src/frontend/js/app/nginx/proxy/main.ejs b/src/frontend/js/app/nginx/proxy/main.ejs index 999dc2d..a5114de 100644 --- a/src/frontend/js/app/nginx/proxy/main.ejs +++ b/src/frontend/js/app/nginx/proxy/main.ejs @@ -3,6 +3,7 @@

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

+ <% if (showAddButton) { %> <%- i18n('proxy-hosts', 'add') %> <% } %> diff --git a/src/frontend/js/app/nginx/proxy/main.js b/src/frontend/js/app/nginx/proxy/main.js index 940b0ab..2e3e9fb 100644 --- a/src/frontend/js/app/nginx/proxy/main.js +++ b/src/frontend/js/app/nginx/proxy/main.js @@ -15,6 +15,7 @@ module.exports = Mn.View.extend({ ui: { list_region: '.list-region', add: '.add-item', + help: '.help', dimmer: '.dimmer' }, @@ -26,6 +27,11 @@ module.exports = Mn.View.extend({ 'click @ui.add': function (e) { e.preventDefault(); App.Controller.showNginxProxyForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('proxy-hosts', 'help-title'), App.i18n('proxy-hosts', 'help-content')); } }, diff --git a/src/frontend/js/app/nginx/redirection/list/item.ejs b/src/frontend/js/app/nginx/redirection/list/item.ejs index de197bf..08bf8c7 100644 --- a/src/frontend/js/app/nginx/redirection/list/item.ejs +++ b/src/frontend/js/app/nginx/redirection/list/item.ejs @@ -28,7 +28,7 @@ diff --git a/src/frontend/js/app/nginx/redirection/main.ejs b/src/frontend/js/app/nginx/redirection/main.ejs index 2cfafab..4345a7e 100644 --- a/src/frontend/js/app/nginx/redirection/main.ejs +++ b/src/frontend/js/app/nginx/redirection/main.ejs @@ -3,6 +3,7 @@

Redirection Hosts

+ <% if (showAddButton) { %> Add Redirection Host <% } %> diff --git a/src/frontend/js/app/nginx/redirection/main.js b/src/frontend/js/app/nginx/redirection/main.js index 146dd72..8955b18 100644 --- a/src/frontend/js/app/nginx/redirection/main.js +++ b/src/frontend/js/app/nginx/redirection/main.js @@ -15,6 +15,7 @@ module.exports = Mn.View.extend({ ui: { list_region: '.list-region', add: '.add-item', + help: '.help', dimmer: '.dimmer' }, @@ -26,6 +27,11 @@ module.exports = Mn.View.extend({ 'click @ui.add': function (e) { e.preventDefault(); App.Controller.showNginxRedirectionForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('redirection-hosts', 'help-title'), App.i18n('redirection-hosts', 'help-content')); } }, diff --git a/src/frontend/js/app/nginx/stream/main.ejs b/src/frontend/js/app/nginx/stream/main.ejs index 84e914b..c01414c 100644 --- a/src/frontend/js/app/nginx/stream/main.ejs +++ b/src/frontend/js/app/nginx/stream/main.ejs @@ -3,6 +3,7 @@

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

+ <% if (showAddButton) { %> <%- i18n('streams', 'add') %> <% } %> diff --git a/src/frontend/js/app/nginx/stream/main.js b/src/frontend/js/app/nginx/stream/main.js index bdf1a44..0dc99c0 100644 --- a/src/frontend/js/app/nginx/stream/main.js +++ b/src/frontend/js/app/nginx/stream/main.js @@ -15,6 +15,7 @@ module.exports = Mn.View.extend({ ui: { list_region: '.list-region', add: '.add-item', + help: '.help', dimmer: '.dimmer' }, @@ -26,6 +27,11 @@ module.exports = Mn.View.extend({ 'click @ui.add': function (e) { e.preventDefault(); App.Controller.showNginxStreamForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('streams', 'help-title'), App.i18n('streams', 'help-content')); } }, diff --git a/src/frontend/js/app/users/list/item.ejs b/src/frontend/js/app/users/list/item.ejs index 3749fc9..4657703 100644 --- a/src/frontend/js/app/users/list/item.ejs +++ b/src/frontend/js/app/users/list/item.ejs @@ -35,7 +35,7 @@ <% if (!isSelf()) { %> - <%- i18n('users', 'delete') %> + <%- i18n('users', 'delete', {name: name}) %> <% } %>
diff --git a/src/frontend/js/app/users/main.ejs b/src/frontend/js/app/users/main.ejs index da5e0d4..8f0d3aa 100644 --- a/src/frontend/js/app/users/main.ejs +++ b/src/frontend/js/app/users/main.ejs @@ -3,7 +3,7 @@

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

diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index e7552d3..4c0b9ec 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -12,6 +12,7 @@ "created-on": "Created: {date}", "save": "Save", "cancel": "Cancel", + "close": "Close", "sure": "Yes I'm Sure", "disabled": "Disabled", "choose-file": "Choose file", @@ -81,7 +82,9 @@ "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-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", + "help-title": "What is a Proxy Host?", + "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager." }, "redirection-hosts": { "title": "Redirection Hosts", @@ -91,13 +94,19 @@ "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}?" + "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?", + "help-title": "What is a Redirection Host?", + "help-content": "A Redirection Host will redirect requests from the incoming domain and push the viewer to another domain.\nThe most common reason to use this type of host is when your website changes domains but you still have search engine or referrer links pointing to the old domain." }, "dead-hosts": { "title": "404 Hosts", "empty": "There are no 404 Hosts", "add": "Add 404 Host", - "form-title": "{id, select, undefined{New} other{Edit}} 404 Host" + "form-title": "{id, select, undefined{New} other{Edit}} 404 Host", + "delete": "Delete 404 Host", + "delete-confirm": "Are you sure you want to delete this 404 Host?", + "help-title": "What is a 404 Host?", + "help-content": "A 404 Host is simply a host setup that shows a 404 page.\nThis can be useful when your domain is listed in search engines and you want to provide a nicer error page or specifically to tell the search indexers that the domain pages no longer exist.\nAnother benefit of having this host is to track the logs for hits to it and view the referrers." }, "streams": { "title": "Streams", @@ -114,7 +123,9 @@ "tcp": "TCP", "udp": "UDP", "delete": "Delete Stream", - "delete-confirm": "Are you sure you want to delete this Stream?" + "delete-confirm": "Are you sure you want to delete this Stream?", + "help-title": "What is a Stream?", + "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy." }, "access-lists": { "title": "Access Lists", @@ -122,7 +133,9 @@ "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" + "public": "Publicly Accessible", + "help-title": "What is an Access List?", + "help-content": "Access Lists provide authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in." }, "users": { "title": "Users", @@ -153,7 +166,19 @@ "audit-log": { "title": "Audit Log", "empty": "There are no logs.", - "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here." + "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here.", + "proxy-host": "Proxy Host", + "redirection-host": "Redirection Host", + "dead-host": "404 Host", + "stream": "Stream", + "user": "User", + "created": "Created {name}", + "updated": "Updated {name}", + "deleted": "Deleted {name}", + "meta-title": "Details for Event", + "view-meta": "View Details", + "action": "Action", + "date": "Date" } } } diff --git a/src/frontend/js/lib/marionette.js b/src/frontend/js/lib/marionette.js index b437538..e028832 100644 --- a/src/frontend/js/lib/marionette.js +++ b/src/frontend/js/lib/marionette.js @@ -15,6 +15,7 @@ Mn.Renderer.render = function (template, data, view) { /** * @param {String} date + * @param {String} format * @returns {String} */ data.formatDbDate = function (date, format) { diff --git a/src/frontend/scss/tabler-extra.scss b/src/frontend/scss/tabler-extra.scss index d9cca6d..ed3c6ee 100644 --- a/src/frontend/scss/tabler-extra.scss +++ b/src/frontend/scss/tabler-extra.scss @@ -86,3 +86,12 @@ $blue: #467fcf; padding: 1rem; } } + +/* modal wide */ + +@media (min-width: 576px) { + .modal-dialog.wide { + max-width: 700px; + margin: 1.75rem auto; + } +}