From c749a22b52ff6964a9cb21fc987f7e95463ac85c Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 7 Aug 2018 20:27:20 +1000 Subject: [PATCH] Certificates into their own section --- package.json | 2 +- src/backend/internal/certificate.js | 95 +++++++++++- src/backend/internal/dead-host.js | 8 +- src/backend/internal/host.js | 2 +- src/backend/internal/proxy-host.js | 42 +---- src/backend/internal/redirection-host.js | 8 +- .../migrations/20180618015850_initial.js | 33 ++-- src/backend/models/certificate.js | 6 +- src/backend/models/dead_host.js | 19 ++- src/backend/models/proxy_host.js | 21 ++- src/backend/models/redirection_host.js | 21 ++- src/backend/routes/api/nginx/certificates.js | 81 +++++++++- src/backend/routes/api/nginx/proxy_hosts.js | 34 ---- .../schema/endpoints/certificates.json | 144 +++++++++++++++++ src/backend/schema/index.json | 3 + src/frontend/js/app/api.js | 18 +-- src/frontend/js/app/audit-log/list/item.ejs | 8 + src/frontend/js/app/controller.js | 26 ++++ .../js/app/nginx/access/list/item.ejs | 2 +- .../js/app/nginx/certificates/form.ejs | 146 ++++++------------ .../js/app/nginx/certificates/form.js | 87 +++-------- .../js/app/nginx/certificates/list/item.ejs | 30 ++-- .../js/app/nginx/certificates/list/item.js | 12 +- .../js/app/nginx/certificates/list/main.ejs | 7 +- .../js/app/nginx/certificates/main.ejs | 10 +- .../js/app/nginx/certificates/main.js | 3 +- src/frontend/js/app/nginx/dead/list/item.ejs | 4 +- src/frontend/js/app/nginx/dead/main.js | 2 +- src/frontend/js/app/nginx/proxy/list/item.ejs | 4 +- src/frontend/js/app/nginx/proxy/main.js | 2 +- .../js/app/nginx/redirection/list/item.ejs | 4 +- src/frontend/js/app/nginx/redirection/main.js | 2 +- .../js/app/nginx/stream/list/item.ejs | 2 +- src/frontend/js/app/users/list/item.ejs | 2 +- src/frontend/js/i18n/messages.json | 20 ++- src/frontend/js/models/certificate.js | 24 ++- src/frontend/js/models/dead-host.js | 18 +-- src/frontend/js/models/proxy-host.js | 15 +- src/frontend/js/models/redirection-host.js | 6 +- src/frontend/js/models/stream.js | 5 +- src/frontend/scss/custom.scss | 6 +- 41 files changed, 599 insertions(+), 385 deletions(-) create mode 100644 src/backend/schema/endpoints/certificates.json diff --git a/package.json b/package.json index 82d1f741..5b8d53dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nginx-proxy-manager", "version": "2.0.0", - "description": "A nice web interface for managing your endpoints", + "description": "A beautiful interface for creating Nginx endpoints", "main": "src/backend/index.js", "devDependencies": { "babel-core": "^6.26.3", diff --git a/src/backend/internal/certificate.js b/src/backend/internal/certificate.js index cb714c0c..ffa0b422 100644 --- a/src/backend/internal/certificate.js +++ b/src/backend/internal/certificate.js @@ -3,6 +3,8 @@ const _ = require('lodash'); const error = require('../lib/error'); const certificateModel = require('../models/certificate'); +const internalAuditLog = require('./audit-log'); +const internalHost = require('./host'); function omissions () { return ['is_deleted']; @@ -17,9 +19,27 @@ const internalCertificate = { */ create: (access, data) => { return access.can('certificates:create', data) - .then(access_data => { - // TODO - return {}; + .then(() => { + data.owner_user_id = access.token.get('attrs').id; + + return certificateModel + .query() + .omit(omissions()) + .insertAndFetch(data); + }) + .then(row => { + data.meta = _.assign({}, data.meta || {}, row.meta); + + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'certificate', + object_id: row.id, + meta: data + }) + .then(() => { + return row; + }); }); }, @@ -135,7 +155,7 @@ const internalCertificate = { .groupBy('id') .omit(['is_deleted']) .allowEager('[owner]') - .orderBy('name', 'ASC'); + .orderBy('nice_name', 'ASC'); if (access_data.permission_visibility !== 'all') { query.andWhere('owner_user_id', access.token.get('attrs').id); @@ -177,6 +197,73 @@ const internalCertificate = { .then(row => { return parseInt(row.count, 10); }); + }, + + /** + * Validates that the certs provided are good + * + * @param {Access} access + * @param {Object} data + * @param {Object} data.files + * @returns {Promise} + */ + validate: (access, data) => { + return new Promise((resolve, reject) => { + let files = {}; + _.map(data.files, (file, name) => { + if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { + files[name] = file.data.toString(); + } + }); + + resolve(files); + }) + .then(files => { + + // TODO: validate using openssl + // files.certificate + // files.certificate_key + + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {Object} data.files + * @returns {Promise} + */ + upload: (access, data) => { + return internalCertificate.get(access, {id: data.id}) + .then(row => { + if (row.provider !== 'other') { + throw new error.ValidationError('Cannot upload certificates for this type of provider'); + } + + _.map(data.files, (file, name) => { + if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { + row.meta[name] = file.data.toString(); + } + }); + + return internalCertificate.update(access, { + id: data.id, + meta: row.meta + }); + }) + .then(row => { + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'certificate', + object_id: row.id, + meta: data + }) + .then(() => { + return _.pick(row.meta, internalHost.allowed_ssl_files); + }); + }); } }; diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index dbcd537a..3d95a6aa 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -41,10 +41,6 @@ const internalDeadHost = { // At this point the domains should have been checked data.owner_user_id = access.token.get('attrs').id; - if (typeof data.meta === 'undefined') { - data.meta = {}; - } - return deadHostModel .query() .omit(omissions()) @@ -149,7 +145,7 @@ const internalDeadHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[owner]') + .allowEager('[owner,certificate]') .first(); if (access_data.permission_visibility !== 'all') { @@ -274,7 +270,7 @@ const internalDeadHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .allowEager('[owner]') + .allowEager('[owner,certificate]') .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/src/backend/internal/host.js b/src/backend/internal/host.js index 3ba159b8..c314c8b0 100644 --- a/src/backend/internal/host.js +++ b/src/backend/internal/host.js @@ -8,7 +8,7 @@ const deadHostModel = require('../models/dead_host'); const internalHost = { - allowed_ssl_files: ['other_certificate', 'other_certificate_key'], + allowed_ssl_files: ['certificate', 'certificate_key'], /** * Internal use only, checks to see if the domain is already taken by any other record diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index f457904d..68ca8220 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -41,10 +41,6 @@ const internalProxyHost = { // At this point the domains should have been checked data.owner_user_id = access.token.get('attrs').id; - if (typeof data.meta === 'undefined') { - data.meta = {}; - } - return proxyHostModel .query() .omit(omissions()) @@ -151,7 +147,7 @@ const internalProxyHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[owner,access_list]') + .allowEager('[owner,access_list,certificate]') .first(); if (access_data.permission_visibility !== 'all') { @@ -226,40 +222,6 @@ const internalProxyHost = { }); }, - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {Object} data.files - * @returns {Promise} - */ - setCerts: (access, data) => { - return internalProxyHost.get(access, {id: data.id}) - .then(row => { - _.map(data.files, (file, name) => { - if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { - row.meta[name] = file.data.toString(); - } - }); - - return internalProxyHost.update(access, { - id: data.id, - meta: row.meta - }); - }) - .then(row => { - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.pick(row.meta, internalHost.allowed_ssl_files); - }); - }); - }, - /** * All Hosts * @@ -276,7 +238,7 @@ const internalProxyHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .allowEager('[owner,access_list]') + .allowEager('[owner,access_list,certificate]') .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index b2ca2bf8..64ece1a3 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -41,10 +41,6 @@ const internalRedirectionHost = { // At this point the domains should have been checked data.owner_user_id = access.token.get('attrs').id; - if (typeof data.meta === 'undefined') { - data.meta = {}; - } - return redirectionHostModel .query() .omit(omissions()) @@ -149,7 +145,7 @@ const internalRedirectionHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[owner]') + .allowEager('[owner,certificate]') .first(); if (access_data.permission_visibility !== 'all') { @@ -274,7 +270,7 @@ const internalRedirectionHost = { .where('is_deleted', 0) .groupBy('id') .omit(['is_deleted']) - .allowEager('[owner]') + .allowEager('[owner,certificate]') .orderBy('domain_names', 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 2333732d..379385b4 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -22,7 +22,7 @@ exports.up = function (knex/*, Promise*/) { table.integer('user_id').notNull().unsigned(); table.string('type', 30).notNull(); table.string('secret').notNull(); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); table.integer('is_deleted').notNull().unsigned().defaultTo(0); }) .then(() => { @@ -72,12 +72,11 @@ exports.up = function (knex/*, Promise*/) { table.string('forward_ip').notNull(); table.integer('forward_port').notNull().unsigned(); table.integer('access_list_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); - table.string('ssl_provider').notNull().defaultTo(''); + table.integer('certificate_id').notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0); table.integer('caching_enabled').notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -92,11 +91,10 @@ exports.up = function (knex/*, Promise*/) { table.json('domain_names').notNull(); table.string('forward_domain_name').notNull(); table.integer('preserve_path').notNull().unsigned().defaultTo(0); - table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); - table.string('ssl_provider').notNull().defaultTo(''); + table.integer('certificate_id').notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -109,10 +107,9 @@ exports.up = function (knex/*, Promise*/) { table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); table.json('domain_names').notNull(); - table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); - table.string('ssl_provider').notNull().defaultTo(''); + table.integer('certificate_id').notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -129,7 +126,7 @@ exports.up = function (knex/*, Promise*/) { table.integer('forwarding_port').notNull().unsigned(); table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -142,7 +139,7 @@ exports.up = function (knex/*, Promise*/) { table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); table.string('name').notNull(); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -154,9 +151,11 @@ exports.up = function (knex/*, Promise*/) { table.dateTime('modified_on').notNull(); table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('name').notNull(); - // TODO - table.json('meta').notNull(); + table.string('provider').notNull(); + table.string('nice_name').notNull().defaultTo(''); + table.json('domain_names').notNull().defaultTo('[]'); + table.dateTime('expires_on').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -169,7 +168,7 @@ exports.up = function (knex/*, Promise*/) { table.integer('access_list_id').notNull().unsigned(); table.string('username').notNull(); table.string('password').notNull(); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { @@ -183,7 +182,7 @@ exports.up = function (knex/*, Promise*/) { table.string('object_type').notNull().defaultTo(''); table.integer('object_id').notNull().unsigned().defaultTo(0); table.string('action').notNull(); - table.json('meta').notNull(); + table.json('meta').notNull().defaultTo('{}'); }); }) .then(() => { diff --git a/src/backend/models/certificate.js b/src/backend/models/certificate.js index 7becf01d..476f7b41 100644 --- a/src/backend/models/certificate.js +++ b/src/backend/models/certificate.js @@ -13,6 +13,10 @@ class Certificate extends Model { $beforeInsert () { this.created_on = Model.raw('NOW()'); this.modified_on = Model.raw('NOW()'); + + if (typeof this.expires_on === 'undefined') { + this.expires_on = Model.raw('NOW()'); + } } $beforeUpdate () { @@ -28,7 +32,7 @@ class Certificate extends Model { } static get jsonAttributes () { - return ['meta']; + return ['domain_names', 'meta']; } static get relationMappings () { diff --git a/src/backend/models/dead_host.js b/src/backend/models/dead_host.js index b98aff06..3eb093a8 100644 --- a/src/backend/models/dead_host.js +++ b/src/backend/models/dead_host.js @@ -3,9 +3,10 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const Certificate = require('./certificate'); Model.knex(db); @@ -44,6 +45,18 @@ class DeadHost extends Model { qb.where('user.is_deleted', 0); qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'dead_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); + } } }; } diff --git a/src/backend/models/proxy_host.js b/src/backend/models/proxy_host.js index 280328aa..6a3bffbe 100644 --- a/src/backend/models/proxy_host.js +++ b/src/backend/models/proxy_host.js @@ -3,10 +3,11 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessList = require('./access_list'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const AccessList = require('./access_list'); +const Certificate = require('./certificate'); Model.knex(db); @@ -61,6 +62,18 @@ class ProxyHost extends Model { qb.where('access_list.is_deleted', 0); qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'proxy_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); + } } }; } diff --git a/src/backend/models/redirection_host.js b/src/backend/models/redirection_host.js index 92f7790d..28f12a56 100644 --- a/src/backend/models/redirection_host.js +++ b/src/backend/models/redirection_host.js @@ -3,9 +3,10 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const Certificate = require('./certificate'); Model.knex(db); @@ -33,7 +34,7 @@ class RedirectionHost extends Model { static get relationMappings () { return { - owner: { + owner: { relation: Model.HasOneRelation, modelClass: User, join: { @@ -44,6 +45,18 @@ class RedirectionHost extends Model { qb.where('user.is_deleted', 0); qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'redirection_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); + } } }; } diff --git a/src/backend/routes/api/nginx/certificates.js b/src/backend/routes/api/nginx/certificates.js index d419bf46..a6328a20 100644 --- a/src/backend/routes/api/nginx/certificates.js +++ b/src/backend/routes/api/nginx/certificates.js @@ -75,7 +75,7 @@ router * /api/nginx/certificates/123 */ router - .route('/:host_id') + .route('/:certificate_id') .options((req, res) => { res.sendStatus(204); }) @@ -88,10 +88,10 @@ router */ .get((req, res, next) => { validator({ - required: ['host_id'], + required: ['certificate_id'], additionalProperties: false, properties: { - host_id: { + certificate_id: { $ref: 'definitions#/definitions/id' }, expand: { @@ -99,12 +99,12 @@ router } } }, { - host_id: req.params.host_id, + certificate_id: req.params.certificate_id, expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) .then(data => { return internalCertificate.get(res.locals.access, { - id: parseInt(data.host_id, 10), + id: parseInt(data.certificate_id, 10), expand: data.expand }); }) @@ -123,7 +123,7 @@ router .put((req, res, next) => { apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body) .then(payload => { - payload.id = parseInt(req.params.host_id, 10); + payload.id = parseInt(req.params.certificate_id, 10); return internalCertificate.update(res.locals.access, payload); }) .then(result => { @@ -139,7 +139,7 @@ router * Update and existing certificate */ .delete((req, res, next) => { - internalCertificate.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)}) .then(result => { res.status(200) .send(result); @@ -147,4 +147,71 @@ router .catch(next); }); +/** + * Upload Certs + * + * /api/nginx/certificates/123/upload + */ +router + .route('/:certificate_id/upload') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * POST /api/nginx/certificates/123/upload + * + * Upload certificates + */validate + .post((req, res, next) => { + if (!req.files) { + res.status(400) + .send({error: 'No files were uploaded'}); + } else { + internalCertificate.upload(res.locals.access, { + id: parseInt(req.params.certificate_id, 10), + files: req.files + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + } + }); + +/** + * Validate Certs before saving + * + * /api/nginx/certificates/validate + */ +router + .route('/validate') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * POST /api/nginx/certificates/validate + * + * Validate certificates + */ + .post((req, res, next) => { + if (!req.files) { + res.status(400) + .send({error: 'No files were uploaded'}); + } else { + internalCertificate.validate(res.locals.access, { + files: req.files + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + } + }); + module.exports = router; diff --git a/src/backend/routes/api/nginx/proxy_hosts.js b/src/backend/routes/api/nginx/proxy_hosts.js index da1dedef..ad69f003 100644 --- a/src/backend/routes/api/nginx/proxy_hosts.js +++ b/src/backend/routes/api/nginx/proxy_hosts.js @@ -147,38 +147,4 @@ router .catch(next); }); -/** - * Specific proxy-host Certificates - * - * /api/nginx/proxy-hosts/123/certificates - */ -router - .route('/:host_id/certificates') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes - - /** - * POST /api/nginx/proxy-hosts/123/certificates - * - * Upload certifications - */ - .post((req, res, next) => { - if (!req.files) { - res.status(400) - .send({error: 'No files were uploaded'}); - } else { - internalProxyHost.setCerts(res.locals.access, { - id: parseInt(req.params.host_id, 10), - files: req.files - }) - .then(result => { - res.status(200) - .send(result); - }) - .catch(next); - } - }); - module.exports = router; diff --git a/src/backend/schema/endpoints/certificates.json b/src/backend/schema/endpoints/certificates.json new file mode 100644 index 00000000..d3294f86 --- /dev/null +++ b/src/backend/schema/endpoints/certificates.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/certificates", + "title": "Certificates", + "description": "Endpoints relating to Certificates", + "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" + }, + "provider": { + "$ref": "../definitions.json#/definitions/ssl_provider" + }, + "nice_name": { + "type": "string", + "description": "Nice Name for the custom certificate" + }, + "domain_names": { + "$ref": "../definitions.json#/definitions/domain_names" + }, + "expires_on": { + "description": "Date and time of expiration", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "letsencrypt_email": { + "type": "string", + "format": "email" + }, + "letsencrypt_agree": { + "type": "boolean" + } + } + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "provider": { + "$ref": "#/definitions/provider" + }, + "nice_name": { + "$ref": "#/definitions/nice_name" + }, + "domain_names": { + "$ref": "#/definitions/domain_names" + }, + "expires_on": { + "$ref": "#/definitions/expires_on" + }, + "meta": { + "$ref": "#/definitions/meta" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of Certificates", + "href": "/nginx/certificates", + "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 Certificate", + "href": "/nginx/certificates", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "provider" + ], + "properties": { + "provider": { + "$ref": "#/definitions/provider" + }, + "nice_name": { + "$ref": "#/definitions/nice_name" + }, + "domain_names": { + "$ref": "#/definitions/domain_names" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing Certificate", + "href": "/nginx/certificates/{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 6fc442ef..53972d4e 100644 --- a/src/backend/schema/index.json +++ b/src/backend/schema/index.json @@ -28,6 +28,9 @@ }, "streams": { "$ref": "endpoints/streams.json" + }, + "certificates": { + "$ref": "endpoints/certificates.json" } } } diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index a45e9c0c..4f27144b 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -323,15 +323,6 @@ module.exports = { */ delete: function (id) { return fetch('delete', 'nginx/proxy-hosts/' + id); - }, - - /** - * @param {Integer} id - * @param {FormData} form_data - * @params {Promise} - */ - setCerts: function (id, form_data) { - return FileUpload('nginx/proxy-hosts/' + id + '/certificates', form_data); } }, @@ -535,6 +526,15 @@ module.exports = { */ delete: function (id) { return fetch('delete', 'nginx/certificates/' + id); + }, + + /** + * @param {Integer} id + * @param {FormData} form_data + * @params {Promise} + */ + upload: function (id, form_data) { + return FileUpload('nginx/certificates/' + id + '/upload', form_data); } } }, diff --git a/src/frontend/js/app/audit-log/list/item.ejs b/src/frontend/js/app/audit-log/list/item.ejs index 499b8070..39dfcfb6 100644 --- a/src/frontend/js/app/audit-log/list/item.ejs +++ b/src/frontend/js/app/audit-log/list/item.ejs @@ -46,6 +46,14 @@ %> <% items.push(meta.name); break; + case 'certificate': + %> <% + if (meta.provider === 'letsencrypt') { + items = meta.domain_names; + } else { + items.push(meta.nice_name); + } + break; } %> <%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %> — diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 11aaaa9c..d1ced789 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -333,6 +333,32 @@ module.exports = { } }, + /** + * Nginx Certificate Form + * + * @param [model] + */ + showNginxCertificateForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { + require(['./main', './nginx/certificates/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + + /** + * Certificate Delete Confirm + * + * @param model + */ + showNginxCertificateDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { + require(['./main', './nginx/certificates/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Audit Log */ diff --git a/src/frontend/js/app/nginx/access/list/item.ejs b/src/frontend/js/app/nginx/access/list/item.ejs index 7a815a07..ccf89e4b 100644 --- a/src/frontend/js/app/nginx/access/list/item.ejs +++ b/src/frontend/js/app/nginx/access/list/item.ejs @@ -26,7 +26,7 @@
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
<% if (canManage) { %> - +