Access polish, import v1 stsarted
This commit is contained in:
parent
7d9e716c7c
commit
8d925deeb0
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,3 +10,5 @@ data/*
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
tmp
|
||||
certbot.log
|
||||
|
||||
|
14
README.md
14
README.md
@ -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
|
||||
|
||||
|
8
TODO.md
8
TODO.md
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
68
src/backend/importer.js
Normal 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();
|
||||
}
|
||||
});
|
||||
};
|
@ -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;
|
||||
})
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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'}),
|
||||
};
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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)})
|
||||
|
@ -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",
|
||||
|
@ -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" %}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -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}) %>
|
||||
|
@ -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) %> – 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>
|
||||
|
@ -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="{"id":0}" <%- certificate_id ? '' : 'selected' %>>None</option>
|
||||
<option selected value="new" data-data="{"id":"new"}">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="{"id":0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
|
||||
<option selected value="new" data-data="{"id":"new"}"><%- i18n('all-hosts', 'new-cert') %></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
13
src/frontend/js/app/nginx/proxy/access-list-item.ejs
Normal file
13
src/frontend/js/app/nginx/proxy/access-list-item.ejs
Normal 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}) %> – 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>
|
@ -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="{"id":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="{"id":0}" <%- certificate_id ? '' : 'selected' %>>None</option>
|
||||
<option selected value="new" data-data="{"id":"new"}">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="{"id":0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
|
||||
<option selected value="new" data-data="{"id":"new"}"><%- i18n('all-hosts', 'new-cert') %></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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({
|
||||
|
@ -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="{"id":0}" <%- certificate_id ? '' : 'selected' %>>None</option>
|
||||
<option selected value="new" data-data="{"id":"new"}">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="{"id":0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
|
||||
<option selected value="new" data-data="{"id":"new"}"><%- i18n('all-hosts', 'new-cert') %></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}",
|
||||
|
Loading…
Reference in New Issue
Block a user