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.lock
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)
![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
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
@ -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
- Free SSL using Let's Encrypt or provide your own custom SSL certificates
- 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
#### Future Features
- Live log tail
## Screenshots

View File

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

View File

@ -3,7 +3,6 @@
There's a few ways to configure this app depending on:
- 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)
### 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.
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
{
@ -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.
**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 \
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",
"compression": "^1.7.2",
"config": "^2.0.1",
"diskdb": "^0.1.17",
"ejs": "^2.6.1",
"express": "^4.16.3",
"express-fileupload": "^0.4.0",
@ -56,7 +57,6 @@
"node-rsa": "^1.0.0",
"objection": "^1.1.10",
"path": "^0.12.7",
"pg": "^7.4.3",
"restler": "^3.4.0",
"signale": "^1.2.1",
"temp-write": "^3.4.0",

View File

@ -4,4 +4,3 @@ mkdir -p /data/letsencrypt-acme-challenge
cd /app
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 \
/var/log/nginx \
/data/{nginx,logs,access} \
/data/nginx/{proxy_host,redirection_host,stream,dead_host,temp} \
/var/lib/nginx/cache/{public,private}
/data/nginx \
/data/logs \
/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
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 () {
const migrate = require('./migrate');
const setup = require('./setup');
const importer = require('./importer');
const app = require('./app');
const apiValidator = require('./lib/validator/api');
const internalCertificate = require('./internal/certificate');
return migrate.latest()
.then(() => {
return setup();
})
.then(setup)
.then(importer)
.then(() => {
return apiValidator.loadSchemas;
})

View File

@ -1,10 +1,16 @@
'use strict';
const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error');
const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const utils = require('../lib/utils');
function omissions () {
return ['is_deleted'];
@ -29,6 +35,8 @@ const internalAccessList = {
});
})
.then(row => {
data.id = row.id;
// Now add the items
let promises = [];
data.items.map(function (item) {
@ -44,26 +52,34 @@ const internalAccessList = {
return Promise.all(promises);
})
.then(row => {
// re-fetch with cert
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: row.id,
id: data.id,
expand: ['owner', 'items']
});
}, true /* <- skip masking */);
})
.then(row => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: data
})
return internalAccessList.build(row)
.then(() => {
return row;
if (row.proxy_host_count) {
return internalNginx.reload();
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
@ -72,15 +88,99 @@ const internalAccessList = {
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.email]
* @param {String} [data.name]
* @param {String} [data.items]
* @return {Promise}
*/
update: (access, data) => {
return access.can('access_lists:update', data.id)
.then(access_data => {
// TODO
return {};
return internalAccessList.get(access, {id: data.id});
})
.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 {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skip_masking]
* @return {Promise}
*/
get: (access, data) => {
get: (access, data, skip_masking) => {
if (typeof data === 'undefined') {
data = {};
}
@ -105,9 +206,12 @@ const internalAccessList = {
.then(access_data => {
let query = accessListModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[owner,items]')
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowEager('[owner,items,proxy_hosts]')
.omit(['access_list.is_deleted'])
.first();
if (access_data.permission_visibility !== 'all') {
@ -127,7 +231,7 @@ const internalAccessList = {
})
.then(row => {
if (row) {
if (typeof row.items !== 'undefined' && row.items) {
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
@ -148,19 +252,66 @@ const internalAccessList = {
delete: (access, data) => {
return access.can('access_lists:delete', data.id)
.then(() => {
return internalAccessList.get(access, {id: data.id});
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items']});
})
.then(row => {
if (!row) {
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
.query()
.where('id', row.id)
.patch({
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(() => {
return true;
@ -180,9 +331,8 @@ const internalAccessList = {
.then(access_data => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_hosts.id) as proxy_host_count'), accessListModel.raw('COUNT(items.id) as item_count'))
.leftJoinRelation('proxy_hosts')
.leftJoinRelation('items')
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.omit(['access_list.is_deleted'])
@ -249,12 +399,89 @@ const internalAccessList = {
maskItems: list => {
if (list && typeof list.items !== 'undefined') {
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 = '';
});
}
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
return internalProxyHost.get(access, {
id: row.id,
expand: ['certificate', 'owner']
expand: ['certificate', 'owner', 'access_list']
});
})
.then(row => {
@ -185,7 +185,7 @@ const internalProxyHost = {
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
expand: ['owner', 'certificate', 'access_list']
})
.then(row => {
// Configure nginx

View File

@ -6,5 +6,6 @@ module.exports = {
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}),
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'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on']);
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
}
},
proxy_hosts: {
@ -68,7 +68,7 @@ class AccessList extends Model {
},
modify: function (qb) {
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
*
* Update and existing access-list
* Delete and existing access-list
*/
.delete((req, res, next) => {
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",
"description": "Deletes a existing Access List",

View File

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

View File

@ -39,7 +39,7 @@
items = meta.domain_names;
break;
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);
break;
case 'user':
@ -47,7 +47,7 @@
items.push(meta.name);
break;
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') {
items = meta.domain_names;
} 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
*/
@ -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
*/

View File

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

View File

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

View File

@ -1,18 +1,18 @@
<div>
<% if (id === 'new') { %>
<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>
<span class="description">with Let's Encrypt</span>
<span class="description"><%- i18n('all-hosts', 'with-le') %></span>
<% } else if (id > 0) { %>
<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>
<span class="description"><%- i18n('ssl', provider) %> &ndash; Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %>
<% } else { %>
<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>
<span class="description">This host will not use HTTPS</span>
<span class="description"><%- i18n('all-hosts', 'no-ssl') %></span>
<% } %>
</div>

View File

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

View File

@ -1,11 +1,12 @@
'use strict';
const Mn = require('backbone.marionette');
const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host');
const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs');
const Helpers = require('../../../lib/helpers');
const Mn = require('backbone.marionette');
const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host');
const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs');
const accessListItemTemplate = require('./access-list-item.ejs');
const Helpers = require('../../../lib/helpers');
require('jquery-serializejson');
require('jquery-mask-plugin');
@ -23,6 +24,7 @@ module.exports = Mn.View.extend({
cancel: 'button.cancel',
save: 'button.save',
certificate_select: 'select[name="certificate_id"]',
access_list_select: 'select[name="access_list_id"]',
ssl_forced: 'input[name="ssl_forced"]',
letsencrypt: '.letsencrypt'
},
@ -140,6 +142,37 @@ module.exports = Mn.View.extend({
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
this.ui.letsencrypt.hide();
this.ui.certificate_select.selectize({

View File

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

View File

@ -68,7 +68,12 @@
"domain-names": "Domain Names",
"cert-provider": "Certificate Provider",
"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": {
"letsencrypt": "Let's Encrypt",
@ -152,12 +157,14 @@
"add": "Add Access List",
"form-title": "{id, select, undefined{New} other{Edit}} 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-sub": "No Access Restrictions",
"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.",
"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": {
"title": "Users",
@ -195,6 +202,7 @@
"stream": "Stream",
"user": "User",
"certificate": "Certificate",
"access-list": "Access List",
"created": "Created {name}",
"updated": "Updated {name}",
"deleted": "Deleted {name}",