From cd80cc8e4db0edcd482ce8fe99be2c21e589a473 Mon Sep 17 00:00:00 2001 From: jbowring Date: Sun, 2 Jun 2024 20:03:28 +0100 Subject: [PATCH] Add certificate to streams database model --- backend/internal/stream.js | 99 ++++++++++++++++--- .../migrations/20240427161436_stream_ssl.js | 38 +++++++ backend/models/stream.js | 25 +++-- frontend/js/app/nginx/stream/main.js | 2 +- 4 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 backend/migrations/20240427161436_stream_ssl.js diff --git a/backend/internal/stream.js b/backend/internal/stream.js index 9f76a1d..526ceba 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -1,10 +1,12 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const utils = require('../lib/utils'); -const streamModel = require('../models/stream'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const {castJsonIfNeed} = require('../lib/helpers'); +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const streamModel = require('../models/stream'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const internalHost = require('./host'); +const {castJsonIfNeed} = require('../lib/helpers'); function omissions () { return ['is_deleted']; @@ -18,6 +20,12 @@ const internalStream = { * @returns {Promise} */ create: (access, data) => { + let create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + return access.can('streams:create', data) .then((/*access_data*/) => { // TODO: At this point the existing ports should have been checked @@ -27,11 +35,40 @@ const internalStream = { data.meta = {}; } + let data_no_domains = structuredClone(data); + + // streams aren't routed by domain name so don't store domain names in the DB + delete data_no_domains.domain_names; + return streamModel .query() - .insertAndFetch(data) + .insertAndFetch(data_no_domains) .then(utils.omitRow(omissions())); }) + .then((row) => { + if (create_certificate) { + return internalCertificate.createQuickCertificate(access, data) + .then((cert) => { + // update host with cert id + return internalStream.update(access, { + id: row.id, + certificate_id: cert.id + }); + }) + .then(() => { + return row; + }); + } else { + return row; + } + }) + .then((row) => { + // re-fetch with cert + return internalStream.get(access, { + id: row.id, + expand: ['certificate', 'owner'] + }); + }) .then((row) => { // Configure nginx return internalNginx.configure(streamModel, 'stream', row) @@ -60,6 +97,12 @@ const internalStream = { * @return {Promise} */ update: (access, data) => { + let create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + return access.can('streams:update', data.id) .then((/*access_data*/) => { // TODO: at this point the existing streams should have been checked @@ -71,6 +114,28 @@ const internalStream = { throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); } + if (create_certificate) { + return internalCertificate.createQuickCertificate(access, { + domain_names: data.domain_names || row.domain_names, + meta: _.assign({}, row.meta, data.meta) + }) + .then((cert) => { + // update host with cert id + data.certificate_id = cert.id; + }) + .then(() => { + return row; + }); + } else { + return row; + } + }) + .then((row) => { + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + data = _.assign({}, { + domain_names: row.domain_names + }, data); + return streamModel .query() .patchAndFetchById(row.id, data) @@ -115,7 +180,7 @@ const internalStream = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowGraph('[owner]') + .allowGraph('[owner,certificate]') .first(); if (access_data.permission_visibility !== 'all') { @@ -132,6 +197,7 @@ const internalStream = { if (!row || !row.id) { throw new error.ItemNotFoundError(data.id); } + row = internalHost.cleanRowCertificateMeta(row); // Custom omissions if (typeof data.omit !== 'undefined' && data.omit !== null) { row = _.omit(row, data.omit); @@ -197,14 +263,14 @@ const internalStream = { .then(() => { return internalStream.get(access, { id: data.id, - expand: ['owner'] + expand: ['certificate', 'owner'] }); }) .then((row) => { if (!row || !row.id) { throw new error.ItemNotFoundError(data.id); } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); + throw new error.ValidationError('Stream is already enabled'); } row.enabled = 1; @@ -250,7 +316,7 @@ const internalStream = { if (!row || !row.id) { throw new error.ItemNotFoundError(data.id); } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); + throw new error.ValidationError('Stream is already disabled'); } row.enabled = 0; @@ -298,7 +364,7 @@ const internalStream = { .query() .where('is_deleted', 0) .groupBy('id') - .allowGraph('[owner]') + .allowGraph('[owner,certificate]') .orderByRaw('CAST(incoming_port AS INTEGER) ASC'); if (access_data.permission_visibility !== 'all') { @@ -317,6 +383,13 @@ const internalStream = { } return query.then(utils.omitRows(omissions())); + }) + .then((rows) => { + if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { + return internalHost.cleanAllRowsCertificateMeta(rows); + } + + return rows; }); }, diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js new file mode 100644 index 0000000..5f47b18 --- /dev/null +++ b/backend/migrations/20240427161436_stream_ssl.js @@ -0,0 +1,38 @@ +const migrate_name = 'stream_ssl'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('stream', (table) => { + table.integer('certificate_id').notNull().unsigned().defaultTo(0); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.table('stream', (table) => { + table.dropColumn('certificate_id'); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; diff --git a/backend/models/stream.js b/backend/models/stream.js index b96ca5a..40fd601 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -1,11 +1,9 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const helpers = require('../lib/helpers'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); +const Model = require('objection').Model; +const db = require('../db'); +const helpers = require('../lib/helpers'); +const User = require('./user'); +const Certificate = require('./certificate'); +const now = require('./now_helper'); Model.knex(db); @@ -64,6 +62,17 @@ class Stream extends Model { modify: function (qb) { qb.where('user.is_deleted', 0); } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'stream.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } } }; } diff --git a/frontend/js/app/nginx/stream/main.js b/frontend/js/app/nginx/stream/main.js index 8a86e58..83bdc15 100644 --- a/frontend/js/app/nginx/stream/main.js +++ b/frontend/js/app/nginx/stream/main.js @@ -88,7 +88,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - view.fetch(['owner']) + view.fetch(['owner', 'certificate']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) {