diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..eda69623 --- /dev/null +++ b/TODO.md @@ -0,0 +1,18 @@ +# TODO + +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 +- 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 + +Testing + +- Access Levels +- Visibility +- Forwarding +- Cert renewals +- Custom certs diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..26268a11 --- /dev/null +++ b/config/README.md @@ -0,0 +1,2 @@ +These files are use in development and are not deployed as part of the final product. + \ No newline at end of file diff --git a/config/my.cnf b/config/my.cnf index 497b6fbe..e3860d70 100644 --- a/config/my.cnf +++ b/config/my.cnf @@ -1,8 +1,7 @@ [mysqld] skip-innodb -default-storage-engine=MyISAM -default-tmp-storage-engine=MyISAM +default-storage-engine=Aria +default-tmp-storage-engine=Aria innodb=OFF symbolic-links=0 log-output=file - diff --git a/doc/example/docker-compose.yml b/doc/example/docker-compose.yml index 853f3e58..ac3b3b67 100644 --- a/doc/example/docker-compose.yml +++ b/doc/example/docker-compose.yml @@ -3,13 +3,14 @@ services: app: image: jc21/nginx-proxy-manager:2 restart: always - network_mode: host volumes: - ./config.json:/app/config/production.json - ./data:/data - ./letsencrypt:/etc/letsencrypt depends_on: - db + links: + - db db: image: mariadb restart: always diff --git a/src/backend/internal/access-list.js b/src/backend/internal/access-list.js index 00f0120c..22b7aa0d 100644 --- a/src/backend/internal/access-list.js +++ b/src/backend/internal/access-list.js @@ -1,8 +1,10 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const accessListModel = require('../models/access_list'); +const _ = require('lodash'); +const error = require('../lib/error'); +const accessListModel = require('../models/access_list'); +const accessListAuthModel = require('../models/access_list_auth'); +const internalAuditLog = require('./audit-log'); function omissions () { return ['is_deleted']; @@ -18,8 +20,51 @@ const internalAccessList = { create: (access, data) => { return access.can('access_lists:create', data) .then(access_data => { - // TODO - return {}; + return accessListModel + .query() + .omit(omissions()) + .insertAndFetch({ + name: data.name, + owner_user_id: access.token.get('attrs').id + }); + }) + .then(row => { + // Now add the items + let promises = []; + data.items.map(function (item) { + promises.push(accessListAuthModel + .query() + .insert({ + access_list_id: row.id, + username: item.username, + password: item.password + }) + ); + }); + + return Promise.all(promises); + }) + .then(row => { + // re-fetch with cert + return internalAccessList.get(access, { + id: row.id, + expand: ['owner', 'items'] + }); + }) + .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 + }) + .then(() => { + return row; + }); }); }, @@ -62,7 +107,7 @@ const internalAccessList = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[owner]') + .allowEager('[owner,items]') .first(); if (access_data.permission_visibility !== 'all') { @@ -82,6 +127,10 @@ const internalAccessList = { }) .then(row => { if (row) { + if (typeof row.items !== 'undefined' && row.items) { + row.items = internalAccessList.maskItems(row.items); + } + return _.omit(row, omissions()); } else { throw new error.ItemNotFoundError(data.id); @@ -134,7 +183,7 @@ const internalAccessList = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .allowEager('[owner]') + .allowEager('[owner,items]') .orderBy('name', 'ASC'); if (access_data.permission_visibility !== 'all') { @@ -153,6 +202,17 @@ const internalAccessList = { } return query; + }) + .then(rows => { + if (rows) { + rows.map(function (row, idx) { + if (typeof row.items !== 'undefined' && row.items) { + rows[idx].items = internalAccessList.maskItems(row.items); + } + }); + } + + return rows; }); }, @@ -177,6 +237,21 @@ const internalAccessList = { .then(row => { return parseInt(row.count, 10); }); + }, + + /** + * @param {Object} list + * @returns {Object} + */ + 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); + list.items[idx].password = ''; + }); + } + + return list; } }; diff --git a/src/backend/internal/certificate.js b/src/backend/internal/certificate.js index 051f5599..0936b04c 100644 --- a/src/backend/internal/certificate.js +++ b/src/backend/internal/certificate.js @@ -41,7 +41,6 @@ const internalCertificate = { return utils.exec(certbot_command + ' renew -q ' + (debug_mode ? '--staging' : '')) .then(result => { logger.info(result); - internalCertificate.interval_processing = false; return internalNginx.reload() .then(() => { @@ -49,6 +48,42 @@ const internalCertificate = { return result; }); }) + .then(() => { + // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times + return certificateModel + .query() + .where('is_deleted', 0) + .andWhere('provider', 'letsencrypt') + .then(certificates => { + if (certificates && certificates.length) { + let promises = []; + + certificates.map(function (certificate) { + promises.push( + internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') + .then(cert_info => { + return certificateModel + .query() + .where('id', certificate.id) + .andWhere('provider', 'letsencrypt') + .patch({ + expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') + }); + }) + .catch(err => { + // Don't want to stop the train here, just log the error + logger.error(err.message); + }) + ); + }); + + return Promise.all(promises); + } + }); + }) + .then(() => { + internalCertificate.interval_processing = false; + }) .catch(err => { logger.error(err); internalCertificate.interval_processing = false; diff --git a/src/backend/models/access_list.js b/src/backend/models/access_list.js index d2e98332..55ff3a33 100644 --- a/src/backend/models/access_list.js +++ b/src/backend/models/access_list.js @@ -3,9 +3,10 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const AccessListAuth = require('./access_list_auth'); Model.knex(db); @@ -44,6 +45,17 @@ class AccessList extends Model { qb.where('user.is_deleted', 0); qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } + }, + items: { + relation: Model.HasManyRelation, + modelClass: AccessListAuth, + join: { + from: 'access_list.id', + to: 'access_list_auth.access_list_id' + }, + modify: function (qb) { + qb.omit(['id', 'created_on', 'modified_on']); + } } }; } diff --git a/src/backend/models/access_list_auth.js b/src/backend/models/access_list_auth.js index be64325c..cb44ce1b 100644 --- a/src/backend/models/access_list_auth.js +++ b/src/backend/models/access_list_auth.js @@ -34,7 +34,7 @@ class AccessListAuth extends Model { return { access_list: { relation: Model.HasOneRelation, - modelClass: './access_list', + modelClass: require('./access_list'), join: { from: 'access_list_auth.access_list_id', to: 'access_list.id' diff --git a/src/backend/routes/api/nginx/access_lists.js b/src/backend/routes/api/nginx/access_lists.js index b514403a..f9560269 100644 --- a/src/backend/routes/api/nginx/access_lists.js +++ b/src/backend/routes/api/nginx/access_lists.js @@ -75,7 +75,7 @@ router * /api/nginx/access-lists/123 */ router - .route('/:host_id') + .route('/:list_id') .options((req, res) => { res.sendStatus(204); }) @@ -88,10 +88,10 @@ router */ .get((req, res, next) => { validator({ - required: ['host_id'], + required: ['list_id'], additionalProperties: false, properties: { - host_id: { + list_id: { $ref: 'definitions#/definitions/id' }, expand: { @@ -99,12 +99,12 @@ router } } }, { - host_id: req.params.host_id, + list_id: req.params.list_id, expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) .then(data => { return internalAccessList.get(res.locals.access, { - id: parseInt(data.host_id, 10), + id: parseInt(data.list_id, 10), expand: data.expand }); }) @@ -123,7 +123,7 @@ router .put((req, res, next) => { apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) .then(payload => { - payload.id = parseInt(req.params.host_id, 10); + payload.id = parseInt(req.params.list_id, 10); return internalAccessList.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing access-list */ .delete((req, res, next) => { - internalAccessList.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)}) .then(result => { res.status(200) .send(result); diff --git a/src/backend/schema/endpoints/access-lists.json b/src/backend/schema/endpoints/access-lists.json new file mode 100644 index 00000000..8a69f3b6 --- /dev/null +++ b/src/backend/schema/endpoints/access-lists.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/access-lists", + "title": "Access Lists", + "description": "Endpoints relating to Access Lists", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "name": { + "type": "string", + "description": "Name of the Access List" + }, + "meta": { + "type": "object" + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "name": { + "$ref": "#/definitions/name" + }, + "meta": { + "$ref": "#/definitions/meta" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Access Lists", + "href": "/nginx/access-lists", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new Access List", + "href": "/nginx/access-list", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "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": 1 + } + } + } + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing Access List", + "href": "/nginx/access-list/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ] +} diff --git a/src/backend/schema/index.json b/src/backend/schema/index.json index 53972d4e..b61509bd 100644 --- a/src/backend/schema/index.json +++ b/src/backend/schema/index.json @@ -31,6 +31,9 @@ }, "certificates": { "$ref": "endpoints/certificates.json" + }, + "access-lists": { + "$ref": "endpoints/access-lists.json" } } } diff --git a/src/frontend/js/app/nginx/access/form.ejs b/src/frontend/js/app/nginx/access/form.ejs index e68b3b15..a85e3968 100644 --- a/src/frontend/js/app/nginx/access/form.ejs +++ b/src/frontend/js/app/nginx/access/form.ejs @@ -12,8 +12,19 @@ - +