From c629deb56c03f6e22aa28a762c9c4a79e34a5dca Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 18 Jul 2018 08:35:49 +1000 Subject: [PATCH 1/5] WIP --- docker-compose.yml | 2 + package.json | 2 + src/backend/internal/dead-host.js | 4 +- src/backend/internal/host.js | 96 +++++++++ src/backend/internal/proxy-host.js | 178 +++++++-------- src/backend/internal/redirection-host.js | 4 +- src/backend/internal/user.js | 1 + src/backend/lib/access.js | 4 +- .../lib/access/proxy_hosts-create.json | 23 ++ .../lib/access/proxy_hosts-delete.json | 23 ++ src/backend/lib/access/proxy_hosts-get.json | 23 ++ .../lib/access/proxy_hosts-update.json | 23 ++ .../migrations/20180618015850_initial.js | 6 +- src/backend/models/access_list.js | 52 +++++ src/backend/models/access_list_auth.js | 51 +++++ src/backend/models/dead_host.js | 6 +- src/backend/models/proxy_host.js | 31 ++- src/backend/models/redirection_host.js | 6 +- src/backend/models/stream.js | 6 +- src/backend/routes/api/nginx/proxy_hosts.js | 6 +- src/backend/schema/definitions.json | 25 +++ src/backend/schema/endpoints/proxy-hosts.json | 202 +++++++++--------- src/frontend/js/app/api.js | 19 ++ src/frontend/js/app/controller.js | 13 ++ src/frontend/js/app/nginx/proxy/delete.ejs | 23 ++ src/frontend/js/app/nginx/proxy/delete.js | 38 ++++ src/frontend/js/app/nginx/proxy/form.ejs | 9 +- src/frontend/js/app/nginx/proxy/form.js | 22 +- src/frontend/js/app/nginx/proxy/list/item.ejs | 32 +-- src/frontend/js/app/nginx/proxy/list/item.js | 45 +--- src/frontend/js/app/nginx/proxy/list/main.ejs | 9 +- src/frontend/js/app/nginx/proxy/list/main.js | 11 +- src/frontend/js/app/nginx/proxy/main.js | 2 +- src/frontend/js/models/proxy-host.js | 8 +- 34 files changed, 710 insertions(+), 295 deletions(-) create mode 100644 src/backend/internal/host.js create mode 100644 src/backend/lib/access/proxy_hosts-create.json create mode 100644 src/backend/lib/access/proxy_hosts-delete.json create mode 100644 src/backend/lib/access/proxy_hosts-get.json create mode 100644 src/backend/lib/access/proxy_hosts-update.json create mode 100644 src/backend/models/access_list.js create mode 100644 src/backend/models/access_list_auth.js create mode 100644 src/frontend/js/app/nginx/proxy/delete.ejs create mode 100644 src/frontend/js/app/nginx/proxy/delete.js diff --git a/docker-compose.yml b/docker-compose.yml index dee8bb50..f108e60d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: volumes: - ./data/letsencrypt:/etc/letsencrypt - .:/srv/app + - ~/.yarnrc:/root/.yarnrc + - ~/.npmrc:/root/.npmrc depends_on: - db links: diff --git a/package.json b/package.json index e52293b5..58dd2084 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "jquery": "^3.3.1", "jquery-mask-plugin": "^1.14.15", "jquery-serializejson": "^2.8.1", + "messageformat": "^2.0.2", + "messageformat-loader": "^0.7.0", "mini-css-extract-plugin": "^0.4.0", "moment": "^2.22.2", "node-sass": "^4.9.0", diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index 8c4636cb..6efaf5d8 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -26,7 +26,7 @@ const internalDeadHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .orderBy('domain_name', 'ASC'); + .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { query.andWhere('owner_user_id', access.token.get('attrs').id); @@ -35,7 +35,7 @@ const internalDeadHost = { // Query is used for searching if (typeof search_query === 'string') { query.where(function () { - this.where('domain_name', 'like', '%' + search_query + '%'); + this.where('domain_names', 'like', '%' + search_query + '%'); }); } diff --git a/src/backend/internal/host.js b/src/backend/internal/host.js new file mode 100644 index 00000000..791954b5 --- /dev/null +++ b/src/backend/internal/host.js @@ -0,0 +1,96 @@ +'use strict'; + +const _ = require('lodash'); +const error = require('../lib/error'); +const proxyHostModel = require('../models/proxy_host'); +const redirectionHostModel = require('../models/redirection_host'); +const deadHostModel = require('../models/dead_host'); + +const internalHost = { + + /** + * Internal use only, checks to see if the domain is already taken by any other record + * + * @param {String} hostname + * @param {String} [ignore_type] 'proxy', 'redirection', 'dead' + * @param {Integer} [ignore_id] Must be supplied if type was also supplied + * @returns {Promise} + */ + isHostnameTaken: function (hostname, ignore_type, ignore_id) { + let promises = [ + proxyHostModel + .query() + .where('is_deleted', 0) + .andWhere('domain_names', 'like', '%' + hostname + '%'), + redirectionHostModel + .query() + .where('is_deleted', 0) + .andWhere('domain_names', 'like', '%' + hostname + '%'), + deadHostModel + .query() + .where('is_deleted', 0) + .andWhere('domain_names', 'like', '%' + hostname + '%') + ]; + + return Promise.all(promises) + .then(promises_results => { + let is_taken = false; + + if (promises_results[0]) { + // Proxy Hosts + if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) { + is_taken = true; + } + } + + if (promises_results[1]) { + // Redirection Hosts + if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) { + is_taken = true; + } + } + + if (promises_results[1]) { + // Dead Hosts + if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) { + is_taken = true; + } + } + + return { + hostname: hostname, + is_taken: is_taken + }; + }); + }, + + /** + * Private call only + * + * @param {String} hostname + * @param {Array} existing_rows + * @param {Integer} [ignore_id] + * @returns {Boolean} + */ + _checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) { + let is_taken = false; + + if (existing_rows && existing_rows.length) { + existing_rows.map(function (existing_row) { + existing_row.domain_names.map(function (existing_hostname) { + // Does this domain match? + if (existing_hostname.toLowerCase() === hostname.toLowerCase()) { + if (!ignore_id || ignore_id !== existing_row.id) { + is_taken = true; + } + } + }); + }); + } + + return is_taken; + } + +}; + +module.exports = internalHost; diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 56a978d0..39ba50b0 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const error = require('../lib/error'); const proxyHostModel = require('../models/proxy_host'); +const internalHost = require('./host'); function omissions () { return ['is_deleted']; @@ -16,60 +17,39 @@ const internalProxyHost = { * @returns {Promise} */ create: (access, data) => { - let auth = data.auth || null; - delete data.auth; - - data.avatar = data.avatar || ''; - data.roles = data.roles || []; - - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - return access.can('proxy_hosts:create', data) - .then(() => { - data.avatar = gravatar.url(data.email, {default: 'mm'}); + .then(access_data => { + // Get a list of the domain names and check each of them against existing records + let domain_name_check_promises = []; - return userModel + data.domain_names.map(function (domain_name) { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); + }); + + return Promise.all(domain_name_check_promises) + .then(check_results => { + check_results.map(function (result) { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } + }); + }); + }) + .then(() => { + // At this point the domains should have been checked + data.owner_user_id = access.token.get('attrs').id; + + if (typeof data.meta === 'undefined') { + data.meta = {}; + } + + return proxyHostModel .query() .omit(omissions()) .insertAndFetch(data); }) - .then(user => { - if (auth) { - return authModel - .query() - .insert({ - user_id: user.id, - type: auth.type, - secret: auth.secret, - meta: {} - }) - .then(() => { - return user; - }); - } else { - return user; - } - }) - .then(user => { - // Create permissions row as well - let is_admin = data.roles.indexOf('admin') !== -1; - - return userPermissionModel - .query() - .insert({ - user_id: user.id, - visibility: is_admin ? 'all' : 'user', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage' - }) - .then(() => { - return internalProxyHost.get(access, {id: user.id, expand: ['permissions']}); - }); + .then(row => { + return _.omit(row, omissions()); }); }, @@ -82,63 +62,49 @@ const internalProxyHost = { * @return {Promise} */ update: (access, data) => { - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - return access.can('proxy_hosts:update', data.id) - .then(() => { + .then(access_data => { + // Get a list of the domain names and check each of them against existing records + let domain_name_check_promises = []; - // Make sure that the user being updated doesn't change their email to another user that is already using it - // 1. get user we want to update - return internalProxyHost.get(access, {id: data.id}) - .then(user => { - - // 2. if email is to be changed, find other users with that email - if (typeof data.email !== 'undefined') { - data.email = data.email.toLowerCase().trim(); - - if (user.email !== data.email) { - return internalProxyHost.isEmailAvailable(data.email, data.id) - .then(available => { - if (!available) { - throw new error.ValidationError('Email address already in use - ' + data.email); - } - - return user; - }); - } - } - - // No change to email: - return user; + if (typeof data.domain_names !== 'undefined') { + data.domain_names.map(function (domain_name) { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id)); }); - }) - .then(user => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); + + return Promise.all(domain_name_check_promises) + .then(check_results => { + check_results.map(function (result) { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } + }); + }); } - - data.avatar = gravatar.url(data.email || user.email, {default: 'mm'}); - - return userModel - .query() - .omit(omissions()) - .patchAndFetchById(user.id, data) - .then(saved_user => { - return _.omit(saved_user, omissions()); - }); }) .then(() => { return internalProxyHost.get(access, {id: data.id}); + }) + .then(row => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return proxyHostModel + .query() + .omit(omissions()) + .patchAndFetchById(row.id, data) + .then(saved_row => { + return _.omit(saved_row, omissions()); + }); }); }, /** * @param {Access} access - * @param {Object} [data] - * @param {Integer} [data.id] Defaults to the token user + * @param {Object} data + * @param {Integer} data.id * @param {Array} [data.expand] * @param {Array} [data.omit] * @return {Promise} @@ -153,14 +119,18 @@ const internalProxyHost = { } return access.can('proxy_hosts:get', data.id) - .then(() => { - let query = userModel + .then(access_data => { + let query = proxyHostModel .query() .where('is_deleted', 0) .andWhere('id', data.id) .allowEager('[permissions]') .first(); + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.get('attrs').id); + } + // Custom omissions if (typeof data.omit !== 'undefined' && data.omit !== null) { query.omit(data.omit); @@ -193,19 +163,14 @@ const internalProxyHost = { .then(() => { return internalProxyHost.get(access, {id: data.id}); }) - .then(user => { - if (!user) { + .then(row => { + if (!row) { throw new error.ItemNotFoundError(data.id); } - // Make sure user can't delete themselves - if (user.id === access.token.get('attrs').id) { - throw new error.PermissionError('You cannot delete yourself.'); - } - - return userModel + return proxyHostModel .query() - .where('id', user.id) + .where('id', row.id) .patch({ is_deleted: 1 }); @@ -231,7 +196,8 @@ const internalProxyHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .orderBy('domain_name', 'ASC'); + .allowEager('[owner,access_list]') + .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { query.andWhere('owner_user_id', access.token.get('attrs').id); @@ -240,7 +206,7 @@ const internalProxyHost = { // Query is used for searching if (typeof search_query === 'string') { query.where(function () { - this.where('domain_name', 'like', '%' + search_query + '%'); + this.where('domain_names', 'like', '%' + search_query + '%'); }); } diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index bb37e977..0ec8ab84 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -26,7 +26,7 @@ const internalProxyHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .orderBy('domain_name', 'ASC'); + .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { query.andWhere('owner_user_id', access.token.get('attrs').id); @@ -35,7 +35,7 @@ const internalProxyHost = { // Query is used for searching if (typeof search_query === 'string') { query.where(function () { - this.where('domain_name', 'like', '%' + search_query + '%'); + this.where('domain_names', 'like', '%' + search_query + '%'); }); } diff --git a/src/backend/internal/user.js b/src/backend/internal/user.js index 7a487b69..9cbf63d0 100644 --- a/src/backend/internal/user.js +++ b/src/backend/internal/user.js @@ -290,6 +290,7 @@ const internalUser = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) + .allowEager('[permissions]') .orderBy('name', 'ASC'); // Query is used for searching diff --git a/src/backend/lib/access.js b/src/backend/lib/access.js index 04bf1964..4b403592 100644 --- a/src/backend/lib/access.js +++ b/src/backend/lib/access.js @@ -301,8 +301,8 @@ module.exports = function (token_string) { }); }) .catch(err => { - //logger.error(err.message); - //logger.error(err.errors); + logger.error(err.message); + logger.error(err.errors); throw new error.PermissionError('Permission Denied', err); }); diff --git a/src/backend/lib/access/proxy_hosts-create.json b/src/backend/lib/access/proxy_hosts-create.json new file mode 100644 index 00000000..3ceb86ca --- /dev/null +++ b/src/backend/lib/access/proxy_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/proxy_hosts-delete.json b/src/backend/lib/access/proxy_hosts-delete.json new file mode 100644 index 00000000..3ceb86ca --- /dev/null +++ b/src/backend/lib/access/proxy_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/proxy_hosts-get.json b/src/backend/lib/access/proxy_hosts-get.json new file mode 100644 index 00000000..10c47465 --- /dev/null +++ b/src/backend/lib/access/proxy_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/lib/access/proxy_hosts-update.json b/src/backend/lib/access/proxy_hosts-update.json new file mode 100644 index 00000000..3ceb86ca --- /dev/null +++ b/src/backend/lib/access/proxy_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index db29e156..893e6480 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -67,7 +67,7 @@ exports.up = function (knex/*, Promise*/) { table.dateTime('modified_on').notNull(); table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('domain_name').notNull(); + table.json('domain_names').notNull(); table.string('forward_ip').notNull(); table.integer('forward_port').notNull().unsigned(); table.integer('access_list_id').notNull().unsigned().defaultTo(0); @@ -88,7 +88,7 @@ exports.up = function (knex/*, Promise*/) { table.dateTime('modified_on').notNull(); table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('domain_name').notNull(); + table.json('domain_names').notNull(); table.string('forward_domain_name').notNull(); table.integer('preserve_path').notNull().unsigned().defaultTo(0); table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); @@ -106,7 +106,7 @@ exports.up = function (knex/*, Promise*/) { table.dateTime('modified_on').notNull(); table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('domain_name').notNull(); + table.json('domain_names').notNull(); table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); table.string('ssl_provider').notNull().defaultTo(''); table.json('meta').notNull(); diff --git a/src/backend/models/access_list.js b/src/backend/models/access_list.js new file mode 100644 index 00000000..d2e98332 --- /dev/null +++ b/src/backend/models/access_list.js @@ -0,0 +1,52 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); + +Model.knex(db); + +class AccessList extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'AccessList'; + } + + static get tableName () { + return 'access_list'; + } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'access_list.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = AccessList; diff --git a/src/backend/models/access_list_auth.js b/src/backend/models/access_list_auth.js new file mode 100644 index 00000000..be64325c --- /dev/null +++ b/src/backend/models/access_list_auth.js @@ -0,0 +1,51 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; + +Model.knex(db); + +class AccessListAuth extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'AccessListAuth'; + } + + static get tableName () { + return 'access_list_auth'; + } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + access_list: { + relation: Model.HasOneRelation, + modelClass: './access_list', + join: { + from: 'access_list_auth.access_list_id', + to: 'access_list.id' + }, + modify: function (qb) { + qb.where('access_list.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); + } + } + }; + } +} + +module.exports = AccessListAuth; diff --git a/src/backend/models/dead_host.js b/src/backend/models/dead_host.js index 26e92921..b98aff06 100644 --- a/src/backend/models/dead_host.js +++ b/src/backend/models/dead_host.js @@ -27,6 +27,10 @@ class DeadHost extends Model { return 'dead_host'; } + static get jsonAttributes () { + return ['domain_names', 'meta']; + } + static get relationMappings () { return { owner: { @@ -38,7 +42,7 @@ class DeadHost extends Model { }, modify: function (qb) { qb.where('user.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } } }; diff --git a/src/backend/models/proxy_host.js b/src/backend/models/proxy_host.js index 488bc723..280328aa 100644 --- a/src/backend/models/proxy_host.js +++ b/src/backend/models/proxy_host.js @@ -3,9 +3,10 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const AccessList = require('./access_list'); Model.knex(db); @@ -13,10 +14,14 @@ class ProxyHost extends Model { $beforeInsert () { this.created_on = Model.raw('NOW()'); this.modified_on = Model.raw('NOW()'); + this.domain_names.sort(); } $beforeUpdate () { this.modified_on = Model.raw('NOW()'); + if (typeof this.domain_names !== 'undefined') { + this.domain_names.sort(); + } } static get name () { @@ -27,9 +32,13 @@ class ProxyHost extends Model { return 'proxy_host'; } + static get jsonAttributes () { + return ['domain_names', 'meta']; + } + static get relationMappings () { return { - owner: { + owner: { relation: Model.HasOneRelation, modelClass: User, join: { @@ -38,7 +47,19 @@ class ProxyHost extends Model { }, modify: function (qb) { qb.where('user.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + }, + access_list: { + relation: Model.HasOneRelation, + modelClass: AccessList, + join: { + from: 'proxy_host.access_list_id', + to: 'access_list.id' + }, + modify: function (qb) { + qb.where('access_list.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); } } }; diff --git a/src/backend/models/redirection_host.js b/src/backend/models/redirection_host.js index 7c5ab9dc..92f7790d 100644 --- a/src/backend/models/redirection_host.js +++ b/src/backend/models/redirection_host.js @@ -27,6 +27,10 @@ class RedirectionHost extends Model { return 'redirection_host'; } + static get jsonAttributes () { + return ['domain_names', 'meta']; + } + static get relationMappings () { return { owner: { @@ -38,7 +42,7 @@ class RedirectionHost extends Model { }, modify: function (qb) { qb.where('user.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } } }; diff --git a/src/backend/models/stream.js b/src/backend/models/stream.js index f002a6c7..7e9f76b9 100644 --- a/src/backend/models/stream.js +++ b/src/backend/models/stream.js @@ -27,6 +27,10 @@ class Stream extends Model { return 'stream'; } + static get jsonAttributes () { + return ['meta']; + } + static get relationMappings () { return { owner: { @@ -38,7 +42,7 @@ class Stream extends Model { }, modify: function (qb) { qb.where('user.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } } }; diff --git a/src/backend/routes/api/nginx/proxy_hosts.js b/src/backend/routes/api/nginx/proxy_hosts.js index 04cc4651..ad69f003 100644 --- a/src/backend/routes/api/nginx/proxy_hosts.js +++ b/src/backend/routes/api/nginx/proxy_hosts.js @@ -104,7 +104,7 @@ router }) .then(data => { return internalProxyHost.get(res.locals.access, { - id: data.host_id, + id: parseInt(data.host_id, 10), expand: data.expand }); }) @@ -123,7 +123,7 @@ router .put((req, res, next) => { apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body) .then(payload => { - payload.id = req.params.host_id; + payload.id = parseInt(req.params.host_id, 10); return internalProxyHost.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing proxy-host */ .delete((req, res, next) => { - internalProxyHost.delete(res.locals.access, {id: req.params.host_id}) + internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) .then(result => { res.status(200) .send(result); diff --git a/src/backend/schema/definitions.json b/src/backend/schema/definitions.json index 43ff5560..e064b4d6 100644 --- a/src/backend/schema/definitions.json +++ b/src/backend/schema/definitions.json @@ -134,6 +134,31 @@ "type": "string", "minLength": 8, "maxLength": 255 + }, + "domain_names": { + "description": "Domain Names separated by a comma", + "example": "*.jc21.com,blog.jc21.com", + "type": "array", + "maxItems": 15, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$" + } + }, + "ssl_enabled": { + "description": "Is SSL Enabled", + "example": true, + "type": "boolean" + }, + "ssl_forced": { + "description": "Is SSL Forced", + "example": false, + "type": "boolean" + }, + "ssl_provider": { + "type": "string", + "pattern": "^(letsencrypt|other)$" } } } diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json index 02f44965..e73ec0ba 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "endpoints/proxy-hosts", - "title": "Users", + "title": "Proxy Hosts", "description": "Endpoints relating to Proxy Hosts", "stability": "stable", "type": "object", @@ -15,49 +15,78 @@ "modified_on": { "$ref": "../definitions.json#/definitions/modified_on" }, - "name": { - "description": "Name", - "example": "Jamie Curnow", + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "forward_ip": { "type": "string", - "minLength": 2, - "maxLength": 100 + "format": "ipv4" }, - "nickname": { - "description": "Nickname", - "example": "Jamie", - "type": "string", - "minLength": 2, - "maxLength": 50 + "forward_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 }, - "email": { - "$ref": "../definitions.json#/definitions/email" + "ssl_enabled": { + "$ref": "../definitions.json#/definitions/ssl_enabled" }, - "avatar": { - "description": "Avatar", - "example": "http://somewhere.jpg", - "type": "string", - "minLength": 2, - "maxLength": 150, - "readOnly": true + "ssl_forced": { + "$ref": "../definitions.json#/definitions/ssl_forced" }, - "roles": { - "description": "Roles", - "example": [ - "admin" - ], - "type": "array" + "ssl_provider": { + "$ref": "../definitions.json#/definitions/ssl_provider" }, - "is_disabled": { - "description": "Is Disabled", - "example": false, - "type": "boolean" + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "letsencrypt_email": { + "type": "string", + "format": "email" + }, + "letsencrypt_agree": { + "type": "boolean" + } + } + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "domain_names": { + "$ref": "#/definitions/domain_names" + }, + "forward_ip": { + "$ref": "#/definitions/forward_ip" + }, + "forward_port": { + "$ref": "#/definitions/forward_port" + }, + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" + }, + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "meta": { + "$ref": "#/definitions/meta" } }, "links": [ { "title": "List", - "description": "Returns a list of Users", - "href": "/users", + "description": "Returns a list of Proxy Hosts", + "href": "/nginx/proxy-hosts", "access": "private", "method": "GET", "rel": "self", @@ -73,8 +102,8 @@ }, { "title": "Create", - "description": "Creates a new User", - "href": "/users", + "description": "Creates a new Proxy Host", + "href": "/nginx/proxy-hosts", "access": "private", "method": "POST", "rel": "create", @@ -84,33 +113,31 @@ "schema": { "type": "object", "required": [ - "name", - "nickname", - "email" + "domain_names", + "forward_ip", + "forward_port" ], "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_ip": { + "$ref": "#/definitions/forward_ip" }, - "email": { - "$ref": "#/definitions/email" + "forward_port": { + "$ref": "#/definitions/forward_port" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -122,8 +149,8 @@ }, { "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Updates a existing Proxy Host", + "href": "/nginx/proxy-hosts/{definitions.identity.example}", "access": "private", "method": "PUT", "rel": "update", @@ -133,20 +160,26 @@ "schema": { "type": "object", "properties": { - "name": { - "$ref": "#/definitions/name" + "domain_names": { + "$ref": "#/definitions/domain_names" }, - "nickname": { - "$ref": "#/definitions/nickname" + "forward_ip": { + "$ref": "#/definitions/forward_ip" }, - "email": { - "$ref": "#/definitions/email" + "forward_port": { + "$ref": "#/definitions/forward_port" }, - "roles": { - "$ref": "#/definitions/roles" + "ssl_enabled": { + "$ref": "#/definitions/ssl_enabled" }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" + "ssl_forced": { + "$ref": "#/definitions/ssl_forced" + }, + "ssl_provider": { + "$ref": "#/definitions/ssl_provider" + }, + "meta": { + "$ref": "#/definitions/meta" } } }, @@ -158,8 +191,8 @@ }, { "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", + "description": "Deletes a existing Proxy Host", + "href": "/nginx/proxy-hosts/{definitions.identity.example}", "access": "private", "method": "DELETE", "rel": "delete", @@ -170,34 +203,5 @@ "type": "boolean" } } - ], - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "avatar": { - "$ref": "#/definitions/avatar" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - } - } + ] } diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index b24d6fa9..0cb7529a 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -264,6 +264,25 @@ module.exports = { */ create: function (data) { return fetch('post', 'nginx/proxy-hosts', data); + }, + + /** + * @param {Object} data + * @param {Integer} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'nginx/proxy-hosts/' + id, data); + }, + + /** + * @param {Integer} id + * @returns {Promise} + */ + delete: function (id) { + return fetch('delete', 'nginx/proxy-hosts/' + id); } }, diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 7de92490..14380807 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -147,6 +147,19 @@ module.exports = { } }, + /** + * Proxy Host Delete Confirm + * + * @param model + */ + showNginxProxyDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { + require(['./main', './nginx/proxy/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Nginx Redirection Hosts */ diff --git a/src/frontend/js/app/nginx/proxy/delete.ejs b/src/frontend/js/app/nginx/proxy/delete.ejs new file mode 100644 index 00000000..9f6d04ef --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/delete.ejs @@ -0,0 +1,23 @@ + diff --git a/src/frontend/js/app/nginx/proxy/delete.js b/src/frontend/js/app/nginx/proxy/delete.js new file mode 100644 index 00000000..bd97c359 --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/delete.js @@ -0,0 +1,38 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./delete.ejs'); +const Controller = require('../../controller'); +const Api = require('../../api'); +const App = require('../../main'); + +require('jquery-serializejson'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + Api.Nginx.ProxyHosts.delete(this.model.get('id')) + .then(() => { + Controller.showNginxProxy(); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + } +}); diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index df94dd60..59198adb 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -13,22 +13,23 @@
+
- - + +
- +
- +
diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index 83a80bdb..92f56ea8 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -11,6 +11,7 @@ const ProxyHostModel = require('../../../models/proxy-host'); require('jquery-serializejson'); require('jquery-mask-plugin'); +require('selectize'); module.exports = Mn.View.extend({ template: template, @@ -18,7 +19,7 @@ module.exports = Mn.View.extend({ ui: { form: 'form', - domain_name: 'input[name="domain_name"]', + domain_names: 'input[name="domain_names"]', forward_ip: 'input[name="forward_ip"]', buttons: '.modal-footer button', cancel: 'button.cancel', @@ -73,6 +74,10 @@ module.exports = Mn.View.extend({ data[idx] = item; }); + if (typeof data.domain_names === 'string' && data.domain_names) { + data.domain_names = data.domain_names.split(','); + } + // Process this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); let method = Api.Nginx.ProxyHosts.create; @@ -118,9 +123,18 @@ module.exports = Mn.View.extend({ this.ui.ssl_enabled.trigger('change'); this.ui.ssl_provider.trigger('change'); - this.ui.domain_name[0].oninvalid = function () { - this.setCustomValidity('Please enter a valid domain name. Domain wildcards are allowed: *.yourdomain.com'); - }; + this.ui.domain_names.selectize({ + delimiter: ',', + persist: false, + maxOptions: 15, + create: function (input) { + return { + value: input, + text: input + }; + }, + createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ + }); }, initialize: function (options) { diff --git a/src/frontend/js/app/nginx/proxy/list/item.ejs b/src/frontend/js/app/nginx/proxy/list/item.ejs index bd4d19e0..282c1a66 100644 --- a/src/frontend/js/app/nginx/proxy/list/item.ejs +++ b/src/frontend/js/app/nginx/proxy/list/item.ejs @@ -1,32 +1,40 @@ -
- +
+
-
<%- name %>
+
+ <% domain_names.map(function(host) { + %> + <%- host %> + <% + }); + %> +
Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %>
-
<%- email %>
+
<%- forward_ip %>:<%- forward_port %>
-
<%- roles.join(', ') %>
+
<%- ssl_enabled && ssl_provider ? ssl_provider : 'HTTP only' %>
+ +
<%- access_list_id ? access_list.name : 'Public' %>
+ +<% if (canManage) { %> +<% } %> \ No newline at end of file diff --git a/src/frontend/js/app/nginx/proxy/list/item.js b/src/frontend/js/app/nginx/proxy/list/item.js index e2a68255..6ed11d39 100644 --- a/src/frontend/js/app/nginx/proxy/list/item.js +++ b/src/frontend/js/app/nginx/proxy/list/item.js @@ -4,7 +4,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,58 +11,24 @@ 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' + edit: 'a.edit', + delete: 'a.delete' }, events: { 'click @ui.edit': function (e) { e.preventDefault(); - Controller.showUserForm(this.model); - }, - - 'click @ui.permissions': function (e) { - e.preventDefault(); - Controller.showUserPermissions(this.model); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - Controller.showUserPasswordForm(this.model); + Controller.showNginxProxyForm(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.showNginxProxyDeleteConfirm(this.model); } }, templateContext: { - isSelf: function () { - return Cache.User.get('id') === this.id; - } + canManage: Cache.User.canManage('proxy_hosts') }, initialize: function () { diff --git a/src/frontend/js/app/nginx/proxy/list/main.ejs b/src/frontend/js/app/nginx/proxy/list/main.ejs index ce893413..16e7b6dc 100644 --- a/src/frontend/js/app/nginx/proxy/list/main.ejs +++ b/src/frontend/js/app/nginx/proxy/list/main.ejs @@ -1,9 +1,12 @@   - Name - Email - Roles + Source + Destination + SSL + Access + <% if (canManage) { %>   + <% } %> diff --git a/src/frontend/js/app/nginx/proxy/list/main.js b/src/frontend/js/app/nginx/proxy/list/main.js index 80b7bd54..1557b74f 100644 --- a/src/frontend/js/app/nginx/proxy/list/main.js +++ b/src/frontend/js/app/nginx/proxy/list/main.js @@ -1,8 +1,9 @@ 'use strict'; -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); +const Mn = require('backbone.marionette'); +const ItemView = require('./item'); +const template = require('./main.ejs'); +const Cache = require('../../../cache'); const TableBody = Mn.CollectionView.extend({ tagName: 'tbody', @@ -21,6 +22,10 @@ module.exports = Mn.View.extend({ } }, + templateContext: { + canManage: Cache.User.canManage('proxy_hosts') + }, + onRender: function () { this.showChildView('body', new TableBody({ collection: this.collection diff --git a/src/frontend/js/app/nginx/proxy/main.js b/src/frontend/js/app/nginx/proxy/main.js index 28a48b1d..1ba3c309 100644 --- a/src/frontend/js/app/nginx/proxy/main.js +++ b/src/frontend/js/app/nginx/proxy/main.js @@ -38,7 +38,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - Api.Nginx.ProxyHosts.getAll() + Api.Nginx.ProxyHosts.getAll(['owner', 'access_list']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) { diff --git a/src/frontend/js/models/proxy-host.js b/src/frontend/js/models/proxy-host.js index 9748c75b..1dca9c3c 100644 --- a/src/frontend/js/models/proxy-host.js +++ b/src/frontend/js/models/proxy-host.js @@ -9,8 +9,7 @@ const model = Backbone.Model.extend({ return { created_on: null, modified_on: null, - owner: null, - domain_name: '', + domain_names: [], forward_ip: '', forward_port: null, access_list_id: null, @@ -19,7 +18,10 @@ const model = Backbone.Model.extend({ ssl_forced: false, caching_enabled: false, block_exploits: false, - meta: [] + meta: [], + // The following are expansions: + owner: null, + access_list: null }; } }); From d49c3ba3afd28a17b4866ef0a475d19c299b1771 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 18 Jul 2018 14:28:41 +1000 Subject: [PATCH 2/5] I18n support, fixed version in footer --- src/frontend/js/app/cache.js | 4 +++- src/frontend/js/app/i18n.js | 25 ++++++++++++++++++++++++ src/frontend/js/app/main.js | 13 ++++++------- src/frontend/js/app/ui/footer/main.ejs | 6 ++++-- src/frontend/js/app/ui/footer/main.js | 4 ++-- src/frontend/js/app/ui/header/main.ejs | 8 ++++---- src/frontend/js/app/ui/header/main.js | 7 ++++--- src/frontend/js/i18n/messages.json | 27 ++++++++++++++++++++++++++ src/frontend/js/lib/marionette.js | 3 +++ webpack.config.js | 13 +++++++++++++ 10 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/frontend/js/app/i18n.js create mode 100644 src/frontend/js/i18n/messages.json diff --git a/src/frontend/js/app/cache.js b/src/frontend/js/app/cache.js index c34d6745..9e6534f1 100644 --- a/src/frontend/js/app/cache.js +++ b/src/frontend/js/app/cache.js @@ -3,7 +3,9 @@ const UserModel = require('../models/user'); let cache = { - User: new UserModel.Model() + User: new UserModel.Model(), + locale: 'en', + version: null }; module.exports = cache; diff --git a/src/frontend/js/app/i18n.js b/src/frontend/js/app/i18n.js new file mode 100644 index 00000000..f6ad7120 --- /dev/null +++ b/src/frontend/js/app/i18n.js @@ -0,0 +1,25 @@ +'use strict'; + +const Cache = ('./cache'); +const messages = require('../i18n/messages.json'); + +/** + * @param {String} namespace + * @param {String} key + * @param {Object} [data] + */ +module.exports = function (namespace, key, data) { + let locale = Cache.locale; + // check that the locale exists + if (typeof messages[locale] === 'undefined') { + locale = 'en'; + } + + if (typeof messages[locale][namespace] !== 'undefined' && typeof messages[locale][namespace][key] !== 'undefined') { + return messages[locale][namespace][key](data); + } else if (locale !== 'en' && typeof messages['en'][namespace] !== 'undefined' && typeof messages['en'][namespace][key] !== 'undefined') { + return messages['en'][namespace][key](data); + } + + return 'INVALID I18N: ' + namespace + '/' + key; +}; diff --git a/src/frontend/js/app/main.js b/src/frontend/js/app/main.js index b0104688..21cdbe34 100644 --- a/src/frontend/js/app/main.js +++ b/src/frontend/js/app/main.js @@ -9,6 +9,7 @@ const Router = require('./router'); const Api = require('./api'); const Tokens = require('./tokens'); const UI = require('./ui/main'); +const i18n = require('./i18n'); const App = Mn.Application.extend({ @@ -16,7 +17,6 @@ const App = Mn.Application.extend({ Api: Api, UI: null, Controller: Controller, - version: null, region: { el: '#app', @@ -24,7 +24,7 @@ const App = Mn.Application.extend({ }, onStart: function (app, options) { - console.log('Welcome to Nginx Proxy Manager'); + console.log(i18n('main', 'welcome')); // Check if token is coming through if (this.getParam('token')) { @@ -34,12 +34,12 @@ const App = Mn.Application.extend({ // Check if we are still logged in by refreshing the token Api.status() .then(result => { - this.version = [result.version.major, result.version.minor, result.version.revision].join('.'); + Cache.version = [result.version.major, result.version.minor, result.version.revision].join('.'); }) .then(Api.Tokens.refresh) .then(this.bootstrap) .then(() => { - console.info('You are logged in'); + console.info(i18n('main', 'logged-in', Cache.User.attributes)); this.bootstrapTimer(); this.refreshTokenTimer(); @@ -60,7 +60,6 @@ const App = Mn.Application.extend({ console.warn('Not logged in:', err.message); Controller.showLogin(); }); - }, History: { @@ -86,7 +85,7 @@ const App = Mn.Application.extend({ let ErrorView = Mn.View.extend({ tagName: 'section', id: 'error', - template: _.template('Error loading stuff. Please reload the app.') + template: _.template(i18n('main', 'unknown-error')) }); this.getRegion().show(new ErrorView()); @@ -130,7 +129,7 @@ const App = Mn.Application.extend({ Api.status() .then(result => { let version = [result.version.major, result.version.minor, result.version.revision].join('.'); - if (version !== this.version) { + if (version !== Cache.version) { document.location.reload(); } }) diff --git a/src/frontend/js/app/ui/footer/main.ejs b/src/frontend/js/app/ui/footer/main.ejs index 2a16c682..e7d0a28c 100644 --- a/src/frontend/js/app/ui/footer/main.ejs +++ b/src/frontend/js/app/ui/footer/main.ejs @@ -3,12 +3,14 @@
- v<%- getVersion() %> © 2018 jc21.com. Theme by Tabler + <%- i18n('footer', 'version', {version: getVersion()}) %> + <%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %> + <%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %>
diff --git a/src/frontend/js/app/ui/footer/main.js b/src/frontend/js/app/ui/footer/main.js index 531ad816..5d00cad1 100644 --- a/src/frontend/js/app/ui/footer/main.js +++ b/src/frontend/js/app/ui/footer/main.js @@ -2,7 +2,7 @@ const Mn = require('backbone.marionette'); const template = require('./main.ejs'); -const App = require('../../main'); +const Cache = require('../../cache'); module.exports = Mn.View.extend({ className: 'container', @@ -10,7 +10,7 @@ module.exports = Mn.View.extend({ templateContext: { getVersion: function () { - return App.version; + return Cache.version || '0.0.0'; } } }); diff --git a/src/frontend/js/app/ui/header/main.ejs b/src/frontend/js/app/ui/header/main.ejs index 1dce8a64..28fa8dba 100644 --- a/src/frontend/js/app/ui/header/main.ejs +++ b/src/frontend/js/app/ui/header/main.ejs @@ -1,7 +1,7 @@
-   Nginx Proxy Manager +   <%- i18n('main', 'app') %>
@@ -9,16 +9,16 @@ - <%- getUserField('nickname', null) || getUserField('name', 'Unknown User') %> + <%- getUserField('nickname', null) || getUserField('name', i18n('main', 'unknown-user')) %> <%- getRole() %> @@ -42,7 +42,7 @@
-

<%- getHostStat('stream') %> Streams

+

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

@@ -57,7 +57,7 @@
-

<%- getHostStat('dead') %> 404 Hosts

+

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

diff --git a/src/frontend/js/app/error/main.ejs b/src/frontend/js/app/error/main.ejs index 13e940fb..f7fd709b 100644 --- a/src/frontend/js/app/error/main.ejs +++ b/src/frontend/js/app/error/main.ejs @@ -3,5 +3,5 @@ <%- message %> <% if (retry) { %> -

Try again +

<%- i18n('str', 'try-again') %> <% } %> diff --git a/src/frontend/js/app/i18n.js b/src/frontend/js/app/i18n.js index f6ad7120..451f1968 100644 --- a/src/frontend/js/app/i18n.js +++ b/src/frontend/js/app/i18n.js @@ -21,5 +21,5 @@ module.exports = function (namespace, key, data) { return messages['en'][namespace][key](data); } - return 'INVALID I18N: ' + namespace + '/' + key; + return '(MISSING: ' + namespace + '/' + key + ')'; }; diff --git a/src/frontend/js/app/main.js b/src/frontend/js/app/main.js index 21cdbe34..85d69634 100644 --- a/src/frontend/js/app/main.js +++ b/src/frontend/js/app/main.js @@ -16,6 +16,7 @@ const App = Mn.Application.extend({ Cache: Cache, Api: Api, UI: null, + i18n: i18n, Controller: Controller, region: { diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index 59198adb..05a7fc25 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -1,13 +1,13 @@
- +
@@ -75,7 +75,7 @@
- +
@@ -84,7 +84,7 @@
@@ -92,19 +92,19 @@
-
Certificate
+
<%- i18n('all-hosts', 'other-certificate') %>
- +
-
Certificate Key
+
<%- i18n('all-hosts', 'other-certificate-key') %>
- +
@@ -116,7 +116,7 @@ diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index 92f56ea8..494caf9e 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -2,12 +2,9 @@ const _ = require('underscore'); const Mn = require('backbone.marionette'); -const template = require('./form.ejs'); -const Controller = require('../../controller'); -const Cache = require('../../cache'); -const Api = require('../../api'); const App = require('../../main'); const ProxyHostModel = require('../../../models/proxy-host'); +const template = require('./form.ejs'); require('jquery-serializejson'); require('jquery-mask-plugin'); @@ -80,11 +77,11 @@ module.exports = Mn.View.extend({ // Process this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - let method = Api.Nginx.ProxyHosts.create; + let method = App.Api.Nginx.ProxyHosts.create; if (this.model.get('id')) { // edit - method = Api.Nginx.ProxyHosts.update; + method = App.Api.Nginx.ProxyHosts.update; data.id = this.model.get('id'); } @@ -92,8 +89,8 @@ module.exports = Mn.View.extend({ .then(result => { view.model.set(result); App.UI.closeModal(function () { - if (method === Api.Nginx.ProxyHosts.create) { - Controller.showNginxProxy(); + if (method === App.Api.Nginx.ProxyHosts.create) { + App.Controller.showNginxProxy(); } }); }) @@ -106,7 +103,7 @@ module.exports = Mn.View.extend({ templateContext: { getLetsencryptEmail: function () { - return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : Cache.User.get('email'); + return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); }, getLetsencryptAgree: function () { diff --git a/src/frontend/js/app/nginx/proxy/main.ejs b/src/frontend/js/app/nginx/proxy/main.ejs index 8b2ed9e7..999dc2d3 100644 --- a/src/frontend/js/app/nginx/proxy/main.ejs +++ b/src/frontend/js/app/nginx/proxy/main.ejs @@ -1,10 +1,10 @@
-

Proxy Hosts

+

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

<% if (showAddButton) { %> - Add Proxy Host + <%- 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 1ba3c309..940b0ab4 100644 --- a/src/frontend/js/app/nginx/proxy/main.js +++ b/src/frontend/js/app/nginx/proxy/main.js @@ -1,14 +1,12 @@ 'use strict'; const Mn = require('backbone.marionette'); +const App = require('../../main'); const ProxyHostModel = require('../../../models/proxy-host'); -const Api = require('../../api'); -const Cache = require('../../cache'); -const Controller = require('../../controller'); const ListView = require('./list/main'); const ErrorView = require('../../error/main'); -const template = require('./main.ejs'); const EmptyView = require('../../empty/main'); +const template = require('./main.ejs'); module.exports = Mn.View.extend({ id: 'nginx-proxy', @@ -27,18 +25,18 @@ module.exports = Mn.View.extend({ events: { 'click @ui.add': function (e) { e.preventDefault(); - Controller.showNginxProxyForm(); + App.Controller.showNginxProxyForm(); } }, templateContext: { - showAddButton: Cache.User.canManage('proxy_hosts') + showAddButton: App.Cache.User.canManage('proxy_hosts') }, onRender: function () { let view = this; - Api.Nginx.ProxyHosts.getAll(['owner', 'access_list']) + App.Api.Nginx.ProxyHosts.getAll(['owner', 'access_list']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) { @@ -46,16 +44,16 @@ module.exports = Mn.View.extend({ collection: new ProxyHostModel.Collection(response) })); } else { - let manage = Cache.User.canManage('proxy_hosts'); + let manage = App.Cache.User.canManage('proxy_hosts'); view.showChildView('list_region', new EmptyView({ - title: 'There are no Proxy Hosts', - subtitle: manage ? 'Why don\'t you create one?' : 'And you don\'t have permission to create one.', - link: manage ? 'Add Proxy Host' : null, + title: App.i18n('proxy-hosts', 'empty'), + subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), + link: manage ? App.i18n('proxy-hosts', 'add') : null, btn_color: 'success', permission: 'proxy_hosts', action: function () { - Controller.showNginxProxyForm(); + App.Controller.showNginxProxyForm(); } })); } @@ -66,7 +64,7 @@ module.exports = Mn.View.extend({ code: err.code, message: err.message, retry: function () { - Controller.showNginxProxy(); + App.Controller.showNginxProxy(); } })); diff --git a/src/frontend/js/app/ui/footer/main.ejs b/src/frontend/js/app/ui/footer/main.ejs index e7d0a28c..562e71c2 100644 --- a/src/frontend/js/app/ui/footer/main.ejs +++ b/src/frontend/js/app/ui/footer/main.ejs @@ -9,7 +9,7 @@
- <%- i18n('footer', 'version', {version: getVersion()}) %> + <%- i18n('main', 'version', {version: getVersion()}) %> <%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %> <%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %>
diff --git a/src/frontend/js/app/ui/header/main.ejs b/src/frontend/js/app/ui/header/main.ejs index 28fa8dba..92eae4cd 100644 --- a/src/frontend/js/app/ui/header/main.ejs +++ b/src/frontend/js/app/ui/header/main.ejs @@ -15,10 +15,10 @@ diff --git a/src/frontend/js/app/nginx/proxy/list/item.ejs b/src/frontend/js/app/nginx/proxy/list/item.ejs index 282c1a66..b1aab6e4 100644 --- a/src/frontend/js/app/nginx/proxy/list/item.ejs +++ b/src/frontend/js/app/nginx/proxy/list/item.ejs @@ -13,27 +13,27 @@ %>
- Created: <%- formatDbDate(created_on, 'Do MMMM YYYY') %> + <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
<%- forward_ip %>:<%- forward_port %>
-
<%- ssl_enabled && ssl_provider ? ssl_provider : 'HTTP only' %>
+
<%- ssl_enabled && ssl_provider ? i18n('ssl', ssl_provider) : i18n('ssl', 'none') %>
-
<%- access_list_id ? access_list.name : 'Public' %>
+
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
<% if (canManage) { %> diff --git a/src/frontend/js/app/nginx/proxy/list/item.js b/src/frontend/js/app/nginx/proxy/list/item.js index 6ed11d39..52a201e8 100644 --- a/src/frontend/js/app/nginx/proxy/list/item.js +++ b/src/frontend/js/app/nginx/proxy/list/item.js @@ -1,10 +1,8 @@ 'use strict'; -const Mn = require('backbone.marionette'); -const Controller = require('../../../controller'); -const Api = require('../../../api'); -const Cache = require('../../../cache'); -const template = require('./item.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../../main'); +const template = require('./item.ejs'); module.exports = Mn.View.extend({ template: template, @@ -18,17 +16,17 @@ module.exports = Mn.View.extend({ events: { 'click @ui.edit': function (e) { e.preventDefault(); - Controller.showNginxProxyForm(this.model); + App.Controller.showNginxProxyForm(this.model); }, 'click @ui.delete': function (e) { e.preventDefault(); - Controller.showNginxProxyDeleteConfirm(this.model); + App.Controller.showNginxProxyDeleteConfirm(this.model); } }, templateContext: { - canManage: Cache.User.canManage('proxy_hosts') + canManage: App.Cache.User.canManage('proxy_hosts') }, initialize: function () { diff --git a/src/frontend/js/app/nginx/proxy/list/main.ejs b/src/frontend/js/app/nginx/proxy/list/main.ejs index 16e7b6dc..f2c64ea3 100644 --- a/src/frontend/js/app/nginx/proxy/list/main.ejs +++ b/src/frontend/js/app/nginx/proxy/list/main.ejs @@ -1,9 +1,9 @@   - Source - Destination - SSL - Access + <%- i18n('str', 'source') %> + <%- i18n('str', 'destination') %> + <%- i18n('str', 'ssl') %> + <%- i18n('str', 'access') %> <% if (canManage) { %>   <% } %> diff --git a/src/frontend/js/app/nginx/proxy/list/main.js b/src/frontend/js/app/nginx/proxy/list/main.js index 1557b74f..64896c1f 100644 --- a/src/frontend/js/app/nginx/proxy/list/main.js +++ b/src/frontend/js/app/nginx/proxy/list/main.js @@ -1,9 +1,9 @@ 'use strict'; const Mn = require('backbone.marionette'); +const App = require('../../../main'); const ItemView = require('./item'); const template = require('./main.ejs'); -const Cache = require('../../../cache'); const TableBody = Mn.CollectionView.extend({ tagName: 'tbody', @@ -23,7 +23,7 @@ module.exports = Mn.View.extend({ }, templateContext: { - canManage: Cache.User.canManage('proxy_hosts') + canManage: App.Cache.User.canManage('proxy_hosts') }, onRender: function () { diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index 7bf726c1..ef5a0dfa 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -14,7 +14,15 @@ "cancel": "Cancel", "sure": "Yes I'm Sure", "disabled": "Disabled", - "choose-file": "Choose file" + "choose-file": "Choose file", + "source": "Source", + "destination": "Destination", + "ssl": "SSL", + "access": "Access", + "public": "Public", + "edit": "Edit", + "delete": "Delete", + "logs": "Logs" }, "login": { "title": "Login to your account" @@ -48,18 +56,20 @@ "all-hosts": { "empty-subtitle": "{manage, select, true{Why don't you create one?} other{And you don't have permission to create one.}}", "details": "Details", - "ssl": "SSL", "enable-ssl": "Enable SSL", "force-ssl": "Force SSL", "domain-names": "Domain Names", "cert-provider": "Certificate Provider", - "other-ssl": "Other", - "letsencrypt": "Let's Encrypt", - "letsencrypt-email": "Email Address for Let's Encrypt", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", "other-certificate": "Certificate", "other-certificate-key": "Certificate Key" }, + "ssl": { + "letsencrypt": "Let's Encrypt", + "other": "Other", + "none": "HTTP only", + "letsencrypt-email": "Email Address for Let's Encrypt", + "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service" + }, "proxy-hosts": { "title": "Proxy Hosts", "empty": "There are no Proxy Hosts", @@ -67,7 +77,9 @@ "form-title": "{id, select, undefined{New} other{Edit}} Proxy Host", "forward-ip": "Forward IP", "forward-port": "Forward Port", - "delete": "Delete Proxy Host" + "delete": "Delete Proxy Host", + "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", + "delete-ssl": "The SSL certificates attached will be removed, this action cannot be recovered." }, "redirection-hosts": { "title": "Redirection Hosts" diff --git a/src/frontend/js/models/dead-host.js b/src/frontend/js/models/dead-host.js index e08c6d66..41b89ee3 100644 --- a/src/frontend/js/models/dead-host.js +++ b/src/frontend/js/models/dead-host.js @@ -7,13 +7,14 @@ const model = Backbone.Model.extend({ defaults: function () { return { - created_on: null, - modified_on: null, - owner: null, - domain_name: '', - ssl_enabled: false, - ssl_provider: false, - meta: [] + id: 0, + created_on: null, + modified_on: null, + owner: null, + domain_name: '', + ssl_enabled: false, + ssl_provider: false, + meta: [] }; } }); diff --git a/src/frontend/js/models/proxy-host.js b/src/frontend/js/models/proxy-host.js index 1dca9c3c..86c99a0b 100644 --- a/src/frontend/js/models/proxy-host.js +++ b/src/frontend/js/models/proxy-host.js @@ -7,6 +7,7 @@ const model = Backbone.Model.extend({ defaults: function () { return { + id: 0, created_on: null, modified_on: null, domain_names: [], diff --git a/src/frontend/js/models/redirection-host.js b/src/frontend/js/models/redirection-host.js index 0496a8bd..131fc949 100644 --- a/src/frontend/js/models/redirection-host.js +++ b/src/frontend/js/models/redirection-host.js @@ -7,6 +7,7 @@ const model = Backbone.Model.extend({ defaults: function () { return { + id: 0, created_on: null, modified_on: null, owner: null, diff --git a/src/frontend/js/models/stream.js b/src/frontend/js/models/stream.js index dac6b2b0..775c37de 100644 --- a/src/frontend/js/models/stream.js +++ b/src/frontend/js/models/stream.js @@ -7,6 +7,7 @@ const model = Backbone.Model.extend({ defaults: function () { return { + id: 0, created_on: null, modified_on: null, owner: null, diff --git a/src/frontend/js/models/user.js b/src/frontend/js/models/user.js index ef383894..00aa0f3f 100644 --- a/src/frontend/js/models/user.js +++ b/src/frontend/js/models/user.js @@ -8,6 +8,7 @@ const model = Backbone.Model.extend({ defaults: function () { return { + id: 0, name: '', nickname: '', email: '',