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 fb446246..8dddd301 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 0ae11026..7bf1449a 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 cb4b8c1d..926773ec 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 cadbc73f..8630f8d0 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 00000000..de163ff0 --- /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 ded58d94..391d6ace 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 7f0d711f..32ecd64c 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 00000000..efa09309 --- /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 603b8fd7..bb8c9c3a 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 9cbf63d0..6a91e98e 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 1580c45c..070952f1 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 00000000..28919b18 --- /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 aeb4c70a..584287bf 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 3ce8d018..153f3536 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 e60273c8..d9f312ee 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 00000000..d136541a --- /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 00000000..f870f2ea --- /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 00000000..4f320360 --- /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 00000000..1c4f91b5 --- /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 00000000..49994a26 --- /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 bd4d19e0..499b8070 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 6766f088..f154931f 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 ce893413..ec3cf2a2 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 69a30a25..b9c2f15d 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 00000000..ce08b908 --- /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 00000000..9ec962bd --- /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 7e806840..66ea7e26 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 00000000..6fb79e66 --- /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 00000000..c5787775 --- /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 140cd490..c245ff4a 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 0214bd70..dc31496b 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 71b3cda3..b8f3c776 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 9951fb79..508280ae 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 151b8076..e8e6a22b 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 b1aab6e4..9621df95 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 999dc2d3..a5114de6 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 940b0ab4..2e3e9fbb 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 de197bf7..08bf8c76 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 2cfafabc..4345a7e8 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 146dd72a..8955b184 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 84e914be..c01414ce 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 bdf1a441..0dc99c02 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 3749fc9b..46577034 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 da5e0d40..8f0d3aaf 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 e7552d3c..4c0b9eca 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 b4375381..e0288321 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 d9cca6dd..ed3c6ee4 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; + } +}