Access polish, import v1 stsarted

This commit is contained in:
Jamie Curnow 2018-08-21 08:33:51 +10:00
parent 7d9e716c7c
commit 8d925deeb0
27 changed files with 525 additions and 120 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ data/*
yarn-error.log yarn-error.log
yarn.lock yarn.lock
tmp tmp
certbot.log

View File

@ -6,11 +6,15 @@
![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge)
![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge)
**NOTE: Version 2 is a work in progress. Not all of the areas are complete and is definitely not ready for production use.**
This project comes as a pre-built docker image that enables you to easily forward to your websites This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
----------
**WARNING: Version 2 a complete rewrite!** If you are using the `latest` docker tag and update to version 2
without preparation, horrible things might happen. Refer to the [Migrating Documentation](doc/MIGRATING.md).
----------
## Features ## Features
@ -18,13 +22,9 @@ running at home or otherwise, including free SSL, without having to know too muc
- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx - Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx
- Free SSL using Let's Encrypt or provide your own custom SSL certificates - Free SSL using Let's Encrypt or provide your own custom SSL certificates
- Access Lists and basic HTTP Authentication for your hosts - Access Lists and basic HTTP Authentication for your hosts
- Advanced Nginx configuration available for super users - -Advanced Nginx configuration available for super users- TODO
- User management, permissions and audit log - User management, permissions and audit log
#### Future Features
- Live log tail
## Screenshots ## Screenshots

View File

@ -2,16 +2,16 @@
In order of importance, somewhat.. In order of importance, somewhat..
- Manual certificate writing to disk and usage in nginx configs - MIGRATING.md - Custom ssl certificate saving to disk and usage in nginx configs
- Access Lists UI and Nginx usage
- Make modal dialogs unclosable in overlay
- Dashboard stats are caching instead of querying - Dashboard stats are caching instead of querying
- Create a nice way of importing from v1 let's encrypt certs and config data - Create a nice way of importing from v1 let's encrypt certs and config data
- UI Log tail - UI Log tail
- Custom Nginx Config Editor
Testing Testing:
- Access Levels - Access Levels
- Adding a proxy host without access to read certs or access lists
- Visibility - Visibility
- Forwarding - Forwarding
- Cert renewals - Cert renewals

View File

@ -3,7 +3,6 @@
There's a few ways to configure this app depending on: There's a few ways to configure this app depending on:
- Whether you use `docker-compose` or vanilla docker - Whether you use `docker-compose` or vanilla docker
- Which Database you want to use (mysql or postgres)
- Which architecture you're running it on (raspberry pi also supported) - Which architecture you're running it on (raspberry pi also supported)
### Configuration File ### Configuration File
@ -12,9 +11,9 @@ There's a few ways to configure this app depending on:
Don't worry, this is easy to do. Don't worry, this is easy to do.
The app requires a configuration file to let it know what database you're using and where it is. The app requires a configuration file to let it know what database you're using.
Here's an example configuration for `mysql`: Here's an example configuration for `mysql` (or mariadb):
```json ```json
{ {
@ -29,22 +28,6 @@ Here's an example configuration for `mysql`:
} }
``` ```
and here's one for `postgres`:
```json
{
"database": {
"engine": "pg",
"version": "7.2",
"host": "127.0.0.1",
"name": "nginxproxymanager",
"user": "nginxproxymanager",
"password": "password123",
"port": 5432
}
}
```
Once you've created your configuration file it's easy to mount it in the docker container, examples below. Once you've created your configuration file it's easy to mount it in the docker container, examples below.
**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. These keys **Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. These keys
@ -138,3 +121,24 @@ docker run -d \
-v /path/to/letsencrypt:/etc/letsencrypt \ -v /path/to/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:2-armhf jc21/nginx-proxy-manager:2-armhf
``` ```
### Initial Run
After the app is running for the first time, the following will happen:
- The database will initialize with table structures
- GPG keys will be generated and saved in the configuration file
- A default admin user will be created
This process can take a couple of minutes depending on your machine.
### Default Administrator User
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.

View File

@ -41,6 +41,7 @@
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"compression": "^1.7.2", "compression": "^1.7.2",
"config": "^2.0.1", "config": "^2.0.1",
"diskdb": "^0.1.17",
"ejs": "^2.6.1", "ejs": "^2.6.1",
"express": "^4.16.3", "express": "^4.16.3",
"express-fileupload": "^0.4.0", "express-fileupload": "^0.4.0",
@ -56,7 +57,6 @@
"node-rsa": "^1.0.0", "node-rsa": "^1.0.0",
"objection": "^1.1.10", "objection": "^1.1.10",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^7.4.3",
"restler": "^3.4.0", "restler": "^3.4.0",
"signale": "^1.2.1", "signale": "^1.2.1",
"temp-write": "^3.4.0", "temp-write": "^3.4.0",

View File

@ -4,4 +4,3 @@ mkdir -p /data/letsencrypt-acme-challenge
cd /app cd /app
node --abort_on_uncaught_exception --max_old_space_size=250 /app/src/backend/index.js node --abort_on_uncaught_exception --max_old_space_size=250 /app/src/backend/index.js

View File

@ -2,9 +2,16 @@
mkdir -p /tmp/nginx/body \ mkdir -p /tmp/nginx/body \
/var/log/nginx \ /var/log/nginx \
/data/{nginx,logs,access} \ /data/nginx \
/data/nginx/{proxy_host,redirection_host,stream,dead_host,temp} \ /data/logs \
/var/lib/nginx/cache/{public,private} /data/access \
/data/nginx/proxy_host \
/data/nginx/redirection_host \
/data/nginx/stream \
/data/nginx/dead_host \
/data/nginx/temp \
/var/lib/nginx/cache/public \
/var/lib/nginx/cache/private
touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log
chown root /tmp/nginx chown root /tmp/nginx

68
src/backend/importer.js Normal file
View File

@ -0,0 +1,68 @@
'use strict';
const fs = require('fs');
const logger = require('./logger').import;
const utils = require('./lib/utils');
module.exports = function () {
return new Promise((resolve, reject) => {
if (fs.existsSync('/config') && !fs.existsSync('/config/v2-imported')) {
logger.info('Beginning import from V1 ...');
// Setup
const batchflow = require('batchflow');
const db = require('diskdb');
module.exports = db.connect('/config', ['hosts', 'access']);
// Create a fake access object
const Access = require('./lib/access');
let access = new Access(null);
resolve(access.load(true)
.then(access => {
// Import access lists first
let lists = db.access.find();
lists.map(list => {
logger.warn('List:', list);
});
})
);
/*
let hosts = db.hosts.find();
hosts.map(host => {
logger.warn('Host:', host);
});
*/
// Looks like we need to import from version 1
// There are numerous parts to this import:
//
// 1. The letsencrypt certificates, the need to be added to the database and files renamed
// 2. The access lists from the previous datastore
// 3. The Hosts from the previous datastore
// get all hosts:
// resolve(db.hosts.find());
// get specific host:
// existing_host = db.hosts.findOne({incoming_port: payload.incoming_port});
// remove host:
// db.hosts.remove({hostname: payload.hostname});
// get all access:
// resolve(db.access.find());
resolve();
} else {
resolve();
}
});
};

View File

@ -7,14 +7,14 @@ const logger = require('./logger').global;
function appStart () { function appStart () {
const migrate = require('./migrate'); const migrate = require('./migrate');
const setup = require('./setup'); const setup = require('./setup');
const importer = require('./importer');
const app = require('./app'); const app = require('./app');
const apiValidator = require('./lib/validator/api'); const apiValidator = require('./lib/validator/api');
const internalCertificate = require('./internal/certificate'); const internalCertificate = require('./internal/certificate');
return migrate.latest() return migrate.latest()
.then(() => { .then(setup)
return setup(); .then(importer)
})
.then(() => { .then(() => {
return apiValidator.loadSchemas; return apiValidator.loadSchemas;
}) })

View File

@ -1,10 +1,16 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error'); const error = require('../lib/error');
const accessListModel = require('../models/access_list'); const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth'); const accessListAuthModel = require('../models/access_list_auth');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const utils = require('../lib/utils');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -29,6 +35,8 @@ const internalAccessList = {
}); });
}) })
.then(row => { .then(row => {
data.id = row.id;
// Now add the items // Now add the items
let promises = []; let promises = [];
data.items.map(function (item) { data.items.map(function (item) {
@ -44,26 +52,34 @@ const internalAccessList = {
return Promise.all(promises); return Promise.all(promises);
}) })
.then(row => { .then(() => {
// re-fetch with cert // re-fetch with expansions
return internalAccessList.get(access, { return internalAccessList.get(access, {
id: row.id, id: data.id,
expand: ['owner', 'items'] expand: ['owner', 'items']
}); }, true /* <- skip masking */);
}) })
.then(row => { .then(row => {
// Audit log // Audit log
data.meta = _.assign({}, data.meta || {}, row.meta); data.meta = _.assign({}, data.meta || {}, row.meta);
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.reload();
}
})
.then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: 'created', action: 'created',
object_type: 'access-list', object_type: 'access-list',
object_id: row.id, object_id: row.id,
meta: data meta: internalAccessList.maskItems(data)
});
}) })
.then(() => { .then(() => {
return row; return internalAccessList.maskItems(row);
}); });
}); });
}, },
@ -72,15 +88,99 @@ const internalAccessList = {
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Integer} data.id * @param {Integer} data.id
* @param {String} [data.email]
* @param {String} [data.name] * @param {String} [data.name]
* @param {String} [data.items]
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
return access.can('access_lists:update', data.id) return access.can('access_lists:update', data.id)
.then(access_data => { .then(access_data => {
// TODO return internalAccessList.get(access, {id: data.id});
return {}; })
.then(row => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
})
.then(() => {
// patch name if specified
if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
.query()
.where({id: data.id})
.patch({
name: data.name
});
}
})
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
data.items.map(function (item) {
if (item.password) {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: data.id,
username: item.username,
password: item.password
})
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
});
let query = accessListAuthModel
.query()
.delete()
.where('access_list_id', data.id);
if (items_to_keep.length) {
query.andWhere('username', 'NOT IN', items_to_keep);
}
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'access-list',
object_id: data.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items']
}, true /* <- skip masking */);
})
.then(row => {
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.reload();
}
})
.then(() => {
return internalAccessList.maskItems(row);
});
}); });
}, },
@ -90,9 +190,10 @@ const internalAccessList = {
* @param {Integer} data.id * @param {Integer} data.id
* @param {Array} [data.expand] * @param {Array} [data.expand]
* @param {Array} [data.omit] * @param {Array} [data.omit]
* @param {Boolean} [skip_masking]
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data, skip_masking) => {
if (typeof data === 'undefined') { if (typeof data === 'undefined') {
data = {}; data = {};
} }
@ -105,9 +206,12 @@ const internalAccessList = {
.then(access_data => { .then(access_data => {
let query = accessListModel let query = accessListModel
.query() .query()
.where('is_deleted', 0) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.andWhere('id', data.id) .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.allowEager('[owner,items]') .where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowEager('[owner,items,proxy_hosts]')
.omit(['access_list.is_deleted'])
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
@ -127,7 +231,7 @@ const internalAccessList = {
}) })
.then(row => { .then(row => {
if (row) { if (row) {
if (typeof row.items !== 'undefined' && row.items) { if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row); row = internalAccessList.maskItems(row);
} }
@ -148,19 +252,66 @@ const internalAccessList = {
delete: (access, data) => { delete: (access, data) => {
return access.can('access_lists:delete', data.id) return access.can('access_lists:delete', data.id)
.then(() => { .then(() => {
return internalAccessList.get(access, {id: data.id}); return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items']});
}) })
.then(row => { .then(row => {
if (!row) { if (!row) {
throw new error.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
return accessListModel return accessListModel
.query() .query()
.where('id', row.id) .where('id', row.id)
.patch({ .patch({
is_deleted: 1 is_deleted: 1
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where('access_list_id', '=', row.id)
.patch({access_list_id: 0})
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0;
}); });
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
})
.then(() => {
return internalNginx.reload();
});
}
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
})
}) })
.then(() => { .then(() => {
return true; return true;
@ -180,9 +331,8 @@ const internalAccessList = {
.then(access_data => { .then(access_data => {
let query = accessListModel let query = accessListModel
.query() .query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_hosts.id) as proxy_host_count'), accessListModel.raw('COUNT(items.id) as item_count')) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.leftJoinRelation('proxy_hosts') .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.leftJoinRelation('items')
.where('access_list.is_deleted', 0) .where('access_list.is_deleted', 0)
.groupBy('access_list.id') .groupBy('access_list.id')
.omit(['access_list.is_deleted']) .omit(['access_list.is_deleted'])
@ -249,12 +399,89 @@ const internalAccessList = {
maskItems: list => { maskItems: list => {
if (list && typeof list.items !== 'undefined') { if (list && typeof list.items !== 'undefined') {
list.items.map(function (val, idx) { list.items.map(function (val, idx) {
list.items[idx].hint = val.password.charAt(0) + ('*').repeat(val.password.length - 1); let repeat_for = 8;
let first_char = '*';
if (typeof val.password !== 'undefined' && val.password) {
repeat_for = val.password.length - 1;
first_char = val.password.charAt(0);
}
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
list.items[idx].password = ''; list.items[idx].password = '';
}); });
} }
return list; return list;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @returns {String}
*/
getFilename: list => {
return '/data/access/' + list.id;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @param {String} list.name
* @param {Array} list.items
* @returns {Promise}
*/
build: list => {
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
return new Promise((resolve, reject) => {
let htpasswd_file = internalAccessList.getFilename(list);
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
// 2. create empty access file
try {
fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
})
.then(htpasswd_file => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
.then((/*result*/) => {
next();
})
.catch(err => {
logger.error(err);
next(err);
});
}
})
.error(err => {
logger.error(err);
reject(err);
})
.end(results => {
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results);
});
});
}
});
} }
}; };

View File

@ -74,7 +74,7 @@ const internalProxyHost = {
// re-fetch with cert // re-fetch with cert
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: row.id, id: row.id,
expand: ['certificate', 'owner'] expand: ['certificate', 'owner', 'access_list']
}); });
}) })
.then(row => { .then(row => {
@ -185,7 +185,7 @@ const internalProxyHost = {
.then(() => { .then(() => {
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'certificate'] expand: ['owner', 'certificate', 'access_list']
}) })
.then(row => { .then(row => {
// Configure nginx // Configure nginx

View File

@ -6,5 +6,6 @@ module.exports = {
express: new Signale({scope: 'Express '}), express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}), access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}), nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '}) ssl: new Signale({scope: 'SSL '}),
import: new Signale({scope: 'Importer'}),
}; };

View File

@ -56,7 +56,7 @@ class AccessList extends Model {
to: 'access_list_auth.access_list_id' to: 'access_list_auth.access_list_id'
}, },
modify: function (qb) { modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on']); qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
} }
}, },
proxy_hosts: { proxy_hosts: {
@ -68,7 +68,7 @@ class AccessList extends Model {
}, },
modify: function (qb) { modify: function (qb) {
qb.where('proxy_host.is_deleted', 0); qb.where('proxy_host.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'meta']); qb.omit(['is_deleted', 'meta']);
} }
} }
}; };

View File

@ -136,7 +136,7 @@ router
/** /**
* DELETE /api/nginx/access-lists/123 * DELETE /api/nginx/access-lists/123
* *
* Update and existing access-list * Delete and existing access-list
*/ */
.delete((req, res, next) => { .delete((req, res, next) => {
internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)}) internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)})

View File

@ -107,6 +107,49 @@
} }
} }
}, },
{
"title": "Update",
"description": "Updates a existing Access List",
"href": "/nginx/access-list/{definitions.identity.example}",
"access": "private",
"method": "PUT",
"rel": "update",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
},
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"$ref": "#/definitions/name"
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 0
}
}
}
}
}
},
"targetSchema": {
"properties": {
"$ref": "#/properties"
}
}
},
{ {
"title": "Delete", "title": "Delete",
"description": "Deletes a existing Access List", "description": "Deletes a existing Access List",

View File

@ -17,7 +17,7 @@ server {
{%- if access_list_id > 0 -%} {%- if access_list_id > 0 -%}
# Access List # Access List
auth_basic "Authorization required"; auth_basic "Authorization required";
auth_basic_user_file /config/access/{{ access_list_id }}; auth_basic_user_file /data/access/{{ access_list_id }};
{%- endif %} {%- endif %}
{% include "_forced_ssl.conf" %} {% include "_forced_ssl.conf" %}

View File

@ -39,7 +39,7 @@
items = meta.domain_names; items = meta.domain_names;
break; break;
case 'access-list': case 'access-list':
%> <span class="text-teal"><i class="fe fe-lock"></i></span> <% %> <span class="text-teal"><i class="fe fe-shield"></i></span> <%
items.push(meta.name); items.push(meta.name);
break; break;
case 'user': case 'user':
@ -47,7 +47,7 @@
items.push(meta.name); items.push(meta.name);
break; break;
case 'certificate': case 'certificate':
%> <span class="text-pink"><i class="fe fe-shield"></i></span> <% %> <span class="text-pink"><i class="fe fe-lock"></i></span> <%
if (meta.provider === 'letsencrypt') { if (meta.provider === 'letsencrypt') {
items = meta.domain_names; items = meta.domain_names;
} else { } else {

View File

@ -91,23 +91,6 @@ module.exports = {
} }
}, },
/**
* Error
*
* @param {Error} err
* @param {String} nice_msg
*/
/*
showError: function (err, nice_msg) {
require(['./main', './error/main'], (App, View) => {
App.UI.showAppContent(new View({
err: err,
nice_msg: nice_msg
}));
});
},
*/
/** /**
* Dashboard * Dashboard
*/ */
@ -319,6 +302,19 @@ module.exports = {
} }
}, },
/**
* Access List Delete Confirm
*
* @param model
*/
showNginxAccessListDeleteConfirm: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
require(['./main', './nginx/access/delete'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},
/** /**
* Nginx Certificates * Nginx Certificates
*/ */

View File

@ -8,6 +8,10 @@
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<%= i18n('access-lists', 'delete-confirm') %> <%= i18n('access-lists', 'delete-confirm') %>
<% if (proxy_host_count) { %>
<br><br>
<%- i18n('access-lists', 'delete-has-hosts', {count: proxy_host_count}) %>
<% } %>
</div> </div>
</div> </div>
</form> </form>

View File

@ -12,7 +12,7 @@
</div> </div>
</td> </td>
<td> <td>
<%- i18n('access-lists', 'item-count', {count: item_count}) %> <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>
</td> </td>
<td> <td>
<%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %> <%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %>

View File

@ -1,18 +1,18 @@
<div> <div>
<% if (id === 'new') { %> <% if (id === 'new') { %>
<div class="title"> <div class="title">
<i class="fe fe-shield text-success"></i> Request a new SSL Certificate <i class="fe fe-lock text-success"></i>
</div> </div>
<span class="description">with Let's Encrypt</span> <span class="description"><%- i18n('all-hosts', 'with-le') %></span>
<% } else if (id > 0) { %> <% } else if (id > 0) { %>
<div class="title"> <div class="title">
<i class="fe fe-shield text-pink"></i> <%- provider === 'other' ? nice_name : domain_names.join(', ') %> <i class="fe fe-lock text-pink"></i> <%- provider === 'other' ? nice_name : domain_names.join(', ') %>
</div> </div>
<span class="description"><%- i18n('ssl', provider) %> &ndash; Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %></span> <span class="description"><%- i18n('ssl', provider) %> &ndash; Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %> <% } else { %>
<div class="title"> <div class="title">
<i class="fe fe-shield-off text-danger"></i> None <i class="fe fe-lock-off text-danger"></i> <%- i18n('all-hosts', 'none') %>
</div> </div>
<span class="description">This host will not use HTTPS</span> <span class="description"><%- i18n('all-hosts', 'no-ssl') %></span>
<% } %> <% } %>
</div> </div>

View File

@ -28,10 +28,10 @@
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
<label class="form-label">SSL Certificate</label> <label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="None"> <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>>None</option> <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}">Request a new SSL Certificate</option> <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -0,0 +1,13 @@
<div>
<% if (id > 0) { %>
<div class="title">
<i class="fe fe-shield text-teal"></i> <%- name %>
</div>
<span class="description"><%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> &ndash; Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %>
<div class="title">
<i class="fe fe-shield-off text-yellow"></i> <%- i18n('access-lists', 'public') %>
</div>
<span class="description"><%- i18n('access-lists', 'public-sub') %></span>
<% } %>
</div>

View File

@ -53,8 +53,8 @@
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'access-list') %></label> <label class="form-label"><%- i18n('proxy-hosts', 'access-list') %></label>
<select name="access_list_id" class="form-control custom-select"> <select name="access_list_id" class="form-control custom-select" placeholder="<%- i18n('access-lists', 'public') %>">
<option value="0" selected="selected"><%- i18n('access-lists', 'public') %></option> <option selected value="0" data-data="{&quot;id&quot;:0}" <%- access_list_id ? '' : 'selected' %>><%- i18n('access-lists', 'public') %></option>
</select> </select>
</div> </div>
</div> </div>
@ -66,10 +66,10 @@
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
<label class="form-label">SSL Certificate</label> <label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="None"> <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>>None</option> <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}">Request a new SSL Certificate</option> <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host'); const ProxyHostModel = require('../../../models/proxy-host');
const template = require('./form.ejs'); const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs');
const accessListItemTemplate = require('./access-list-item.ejs');
const Helpers = require('../../../lib/helpers'); const Helpers = require('../../../lib/helpers');
require('jquery-serializejson'); require('jquery-serializejson');
@ -23,6 +24,7 @@ module.exports = Mn.View.extend({
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save',
certificate_select: 'select[name="certificate_id"]', certificate_select: 'select[name="certificate_id"]',
access_list_select: 'select[name="access_list_id"]',
ssl_forced: 'input[name="ssl_forced"]', ssl_forced: 'input[name="ssl_forced"]',
letsencrypt: '.letsencrypt' letsencrypt: '.letsencrypt'
}, },
@ -140,6 +142,37 @@ module.exports = Mn.View.extend({
createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/
}); });
// Access Lists
this.ui.letsencrypt.hide();
this.ui.access_list_select.selectize({
valueField: 'id',
labelField: 'name',
searchField: ['name'],
create: false,
preload: true,
allowEmptyOption: true,
render: {
option: function (item) {
item.i18n = App.i18n;
item.formatDbDate = Helpers.formatDbDate;
return accessListItemTemplate(item);
}
},
load: function (query, callback) {
App.Api.Nginx.AccessLists.getAll(['items'])
.then(rows => {
callback(rows);
})
.catch(err => {
console.error(err);
callback();
});
},
onLoad: function () {
view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id'));
}
});
// Certificates // Certificates
this.ui.letsencrypt.hide(); this.ui.letsencrypt.hide();
this.ui.certificate_select.selectize({ this.ui.certificate_select.selectize({

View File

@ -52,10 +52,10 @@
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<div class="form-group"> <div class="form-group">
<label class="form-label">SSL Certificate</label> <label class="form-label"><%- i18n('all-hosts', 'ssl-certificate') %></label>
<select name="certificate_id" class="form-control custom-select" placeholder="None"> <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
<option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>>None</option> <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
<option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}">Request a new SSL Certificate</option> <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -68,7 +68,12 @@
"domain-names": "Domain Names", "domain-names": "Domain Names",
"cert-provider": "Certificate Provider", "cert-provider": "Certificate Provider",
"block-exploits": "Block Common Exploits", "block-exploits": "Block Common Exploits",
"caching-enabled": "Cache Assets" "caching-enabled": "Cache Assets",
"ssl-certificate": "SSL Certificate",
"none": "None",
"new-cert": "Request a new SSL Certificate",
"with-le": "with Let's Encrypt",
"no-ssl": "This host will not use HTTPS"
}, },
"ssl": { "ssl": {
"letsencrypt": "Let's Encrypt", "letsencrypt": "Let's Encrypt",
@ -152,12 +157,14 @@
"add": "Add Access List", "add": "Add Access List",
"form-title": "{id, select, undefined{New} other{Edit}} Access List", "form-title": "{id, select, undefined{New} other{Edit}} Access List",
"delete": "Delete 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.", "delete-confirm": "Are you sure you want to delete this access list?",
"public": "Publicly Accessible", "public": "Publicly Accessible",
"public-sub": "No Access Restrictions",
"help-title": "What is an Access List?", "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.", "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.",
"item-count": "{count} {count, select, 1{User} other{Users}}", "item-count": "{count} {count, select, 1{User} other{Users}}",
"proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}" "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}",
"delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion."
}, },
"users": { "users": {
"title": "Users", "title": "Users",
@ -195,6 +202,7 @@
"stream": "Stream", "stream": "Stream",
"user": "User", "user": "User",
"certificate": "Certificate", "certificate": "Certificate",
"access-list": "Access List",
"created": "Created {name}", "created": "Created {name}",
"updated": "Updated {name}", "updated": "Updated {name}",
"deleted": "Deleted {name}", "deleted": "Deleted {name}",