diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 32f2bc0dc..5945bfef1 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -49,6 +49,15 @@ const internalProxyHost = { data.owner_user_id = access.token.getUserId(1); data = internalHost.cleanSslHstsData(data); + // If upstream is used, clear forwarding fields + if (data.upstream_id) { + data.forward_host = ''; + data.forward_port = 0; + } + + // This is a UI-only field, remove it + delete data.forward_to_type; + // Fix for db field not having a default value // for this optional field. if (typeof data.advanced_config === 'undefined') { @@ -81,7 +90,7 @@ const internalProxyHost = { // re-fetch with cert return internalProxyHost.get(access, { id: row.id, - expand: ['certificate', 'owner', 'access_list.[clients,items]'] + expand: ['certificate', 'owner', 'access_list.[clients,items]', 'upstream'] }); }) .then((row) => { @@ -174,6 +183,18 @@ const internalProxyHost = { data = internalHost.cleanSslHstsData(data, row); + // If upstream is used, clear forwarding fields + if (data.upstream_id) { + data.forward_host = ''; + data.forward_port = 0; + } else if (data.upstream_id === 0) { + // Upstream was removed, make sure it's cleared + data.upstream_id = 0; + } + + // This is a UI-only field, remove it + delete data.forward_to_type; + return proxyHostModel .query() .where({id: data.id}) @@ -195,7 +216,7 @@ const internalProxyHost = { .then(() => { return internalProxyHost.get(access, { id: data.id, - expand: ['owner', 'certificate', 'access_list.[clients,items]'] + expand: ['owner', 'certificate', 'access_list.[clients,items]', 'upstream'] }) .then((row) => { if (!row.enabled) { @@ -232,7 +253,7 @@ const internalProxyHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowGraph('[owner,access_list.[clients,items],certificate]') + .allowGraph('[owner,access_list.[clients,items],certificate,upstream]') .first(); if (access_data.permission_visibility !== 'all') { @@ -315,7 +336,7 @@ const internalProxyHost = { .then(() => { return internalProxyHost.get(access, { id: data.id, - expand: ['certificate', 'owner', 'access_list'] + expand: ['certificate', 'owner', 'access_list', 'upstream'] }); }) .then((row) => { @@ -416,7 +437,7 @@ const internalProxyHost = { .query() .where('is_deleted', 0) .groupBy('id') - .allowGraph('[owner,access_list,certificate]') + .allowGraph('[owner,access_list,certificate,upstream]') .orderBy(castJsonIfNeed('domain_names'), 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/backend/internal/upstream.js b/backend/internal/upstream.js new file mode 100644 index 000000000..30412f5de --- /dev/null +++ b/backend/internal/upstream.js @@ -0,0 +1,175 @@ +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const upstreamModel = require('../models/upstream'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const proxyHostModel = require('../models/proxy_host'); + +function omissions() { + return ['is_deleted']; +} + +const internalUpstream = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + return access.can('upstreams:create', data) + .then(() => { + data.owner_user_id = access.token.getUserId(1); + return upstreamModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); + }) + .then((row) => { + // Audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'upstream', + object_id: row.id, + meta: data + }).then(() => row); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + update: (access, data) => { + return access.can('upstreams:update', data.id) + .then(() => { + return internalUpstream.get(access, { id: data.id }); + }) + .then((row) => { + if (row.id !== data.id) { + throw new error.InternalValidationError('Upstream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return upstreamModel.query().patchAndFetchById(row.id, data).then(utils.omitRow(omissions())); + }) + .then((row) => { + // Audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'upstream', + object_id: row.id, + meta: data + }).then(() => row); + }) + .then((row) => { + // Find all proxy hosts using this upstream and re-generate their configs + return proxyHostModel.query() + .where('upstream_id', row.id) + .andWhere('is_deleted', 0) + .withGraphFetched('[certificate,access_list,upstream]') // The fix is here: use withGraphFetched + .then((hosts) => { + if (hosts && hosts.length) { + return internalNginx.bulkGenerateConfigs('proxy_host', hosts) + .then(internalNginx.reload) + .then(() => row); + } + return row; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + get: (access, data) => { + if (typeof data === 'undefined') { + data = {}; + } + + return access.can('upstreams:get', data.id) + .then((access_data) => { + let query = upstreamModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowGraph('[owner]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched(`[${data.expand.join(', ')}]`); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + return row; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + delete: (access, data) => { + return access.can('upstreams:delete', data.id) + .then(() => { + return internalUpstream.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + + return upstreamModel.query() + .where('id', row.id) + .patch({ is_deleted: 1 }); + }) + .then(() => { + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'upstream', + object_id: data.id + }); + }) + .then(() => true); + }, + + /** + * @param {Access} access + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('upstreams:list') + .then((access_data) => { + let query = upstreamModel + .query() + .where('is_deleted', 0) + .allowGraph('[owner]') + .orderBy('name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof search_query === 'string' && search_query) { + query.where('name', 'like', `%${search_query}%`); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched(`[${expand.join(', ')}]`); + } + + return query.then(utils.omitRows(omissions())); + }); + } +}; + +module.exports = internalUpstream; diff --git a/backend/internal/user.js b/backend/internal/user.js index 742ab65d3..132680ebb 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -70,7 +70,8 @@ const internalUser = { dead_hosts: 'manage', streams: 'manage', access_lists: 'manage', - certificates: 'manage' + certificates: 'manage', + upstreams: 'manage' }) .then(() => { return internalUser.get(access, {id: user.id, expand: ['permissions']}); diff --git a/backend/lib/access.js b/backend/lib/access.js index 0e658a656..fc653373b 100644 --- a/backend/lib/access.js +++ b/backend/lib/access.js @@ -261,7 +261,8 @@ module.exports = function (token_string) { permission_dead_hosts: permissions.dead_hosts, permission_streams: permissions.streams, permission_access_lists: permissions.access_lists, - permission_certificates: permissions.certificates + permission_certificates: permissions.certificates, + permission_upstreams: permissions.upstreams } }; diff --git a/backend/lib/access/upstreams-create.json b/backend/lib/access/upstreams-create.json new file mode 100644 index 000000000..07eacd169 --- /dev/null +++ b/backend/lib/access/upstreams-create.json @@ -0,0 +1,28 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "permission_upstreams", + "roles" + ], + "properties": { + "permission_upstreams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user" + ] + } + } + } + } + ] +} diff --git a/backend/lib/access/upstreams-delete.json b/backend/lib/access/upstreams-delete.json new file mode 100644 index 000000000..07eacd169 --- /dev/null +++ b/backend/lib/access/upstreams-delete.json @@ -0,0 +1,28 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "permission_upstreams", + "roles" + ], + "properties": { + "permission_upstreams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user" + ] + } + } + } + } + ] +} diff --git a/backend/lib/access/upstreams-get.json b/backend/lib/access/upstreams-get.json new file mode 100644 index 000000000..d08b1e4b8 --- /dev/null +++ b/backend/lib/access/upstreams-get.json @@ -0,0 +1,28 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "permission_upstreams", + "roles" + ], + "properties": { + "permission_upstreams": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user" + ] + } + } + } + } + ] +} diff --git a/backend/lib/access/upstreams-list.json b/backend/lib/access/upstreams-list.json new file mode 100644 index 000000000..d08b1e4b8 --- /dev/null +++ b/backend/lib/access/upstreams-list.json @@ -0,0 +1,28 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "permission_upstreams", + "roles" + ], + "properties": { + "permission_upstreams": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user" + ] + } + } + } + } + ] +} diff --git a/backend/lib/access/upstreams-update.json b/backend/lib/access/upstreams-update.json new file mode 100644 index 000000000..07eacd169 --- /dev/null +++ b/backend/lib/access/upstreams-update.json @@ -0,0 +1,28 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "permission_upstreams", + "roles" + ], + "properties": { + "permission_upstreams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user" + ] + } + } + } + } + ] +} diff --git a/backend/migrations/20250825073649_upstreams.js b/backend/migrations/20250825073649_upstreams.js new file mode 100755 index 000000000..d33754a9b --- /dev/null +++ b/backend/migrations/20250825073649_upstreams.js @@ -0,0 +1,71 @@ +const migrate_name = 'upstreams'; +const logger = require('../logger').migrate; + +/** + * Migrate + * @see http://knexjs.org/#Schema + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.createTable('upstream', (table) => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('name').notNull(); + table.string('scheme').notNull().defaultTo('http'); + table.json('servers').notNull(); + table.json('meta').notNull(); + }) + .then(() => { + logger.info('[' + migrate_name + '] upstream Table created'); + return knex.schema.table('proxy_host', (table) => { + table.string('forward_host').nullable().alter(); + table.integer('forward_port').nullable().alter(); + table.integer('upstream_id').notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + return knex.schema.table('user_permission', (table) => { + table.string('upstreams').notNull().defaultTo('hidden'); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] user_permission Table altered'); + }); +}; + +/** + * Undo Migrate + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.dropTable('upstream') + .then(() => { + logger.info('[' + migrate_name + '] upstream Table dropped'); + return knex.schema.table('proxy_host', (table) => { + table.string('forward_host').notNullable().alter(); + table.integer('forward_port').notNullable().alter(); + table.dropColumn('upstream_id'); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + return knex.schema.table('user_permission', (table) => { + table.dropColumn('upstreams'); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] user_permission Table altered'); + }); +}; \ No newline at end of file diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 07aa5dd3c..ec97c011e 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -7,6 +7,7 @@ const Model = require('objection').Model; const User = require('./user'); const AccessList = require('./access_list'); const Certificate = require('./certificate'); +const Upstream = require('./upstream'); const now = require('./now_helper'); Model.knex(db); @@ -106,6 +107,17 @@ class ProxyHost extends Model { modify: function (qb) { qb.where('certificate.is_deleted', 0); } + }, + upstream: { + relation: Model.HasOneRelation, + modelClass: Upstream, + join: { + from: 'proxy_host.upstream_id', + to: 'upstream.id' + }, + modify: function (qb) { + qb.where('upstream.is_deleted', 0); + } } }; } diff --git a/backend/models/upstream.js b/backend/models/upstream.js new file mode 100644 index 000000000..a4e6d15be --- /dev/null +++ b/backend/models/upstream.js @@ -0,0 +1,70 @@ +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const User = require('./user'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', +]; + +class Upstream extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + if (typeof this.meta === 'undefined') { + this.meta = {}; + } + + if (typeof this.servers === 'undefined') { + this.servers = []; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + static get name() { + return 'Upstream'; + } + + static get tableName() { + return 'upstream'; + } + + static get jsonAttributes() { + return ['servers', 'meta']; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'upstream.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + } + } + }; + } +} + +module.exports = Upstream; diff --git a/backend/routes/main.js b/backend/routes/main.js index b97096d0e..83c83028e 100644 --- a/backend/routes/main.js +++ b/backend/routes/main.js @@ -37,6 +37,7 @@ router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); router.use('/nginx/streams', require('./nginx/streams')); router.use('/nginx/access-lists', require('./nginx/access_lists')); router.use('/nginx/certificates', require('./nginx/certificates')); +router.use('/nginx/upstreams', require('./nginx/upstreams')); /** * API 404 for all other routes diff --git a/backend/routes/nginx/upstreams.js b/backend/routes/nginx/upstreams.js new file mode 100644 index 000000000..6a615e902 --- /dev/null +++ b/backend/routes/nginx/upstreams.js @@ -0,0 +1,73 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const internalUpstream = require('../../internal/upstream'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +router + .route('/') + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { $ref: 'common#/properties/expand' }, + query: { $ref: 'common#/properties/query' } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then((data) => internalUpstream.getAll(res.locals.access, data.expand, data.query)) + .then((rows) => res.status(200).send(rows)) + .catch(next); + }) + .post((req, res, next) => { + internalUpstream.create(res.locals.access, req.body) + .then((result) => res.status(201).send(result)) + .catch(next); + }); + +router + .route('/:id') + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .get((req, res, next) => { + validator({ + required: ['id'], + additionalProperties: false, + properties: { + id: { $ref: 'common#/properties/id' }, + expand: { $ref: 'common#/properties/expand' } + } + }, { + id: req.params.id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then((data) => internalUpstream.get(res.locals.access, { id: parseInt(data.id, 10), expand: data.expand })) + .then((row) => res.status(200).send(row)) + .catch(next); + }) + .put((req, res, next) => { + req.body.id = parseInt(req.params.id, 10); + internalUpstream.update(res.locals.access, req.body) + .then((result) => res.status(200).send(result)) + .catch(next); + }) + .delete((req, res, next) => { + internalUpstream.delete(res.locals.access, { id: parseInt(req.params.id, 10) }) + .then((result) => res.status(200).send(result)) + .catch(next); + }); + +module.exports = router; diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index e9dcacb5e..b74b2502b 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -22,7 +22,8 @@ "enabled", "locations", "hsts_enabled", - "hsts_subdomains" + "hsts_subdomains", + "upstream_id" ], "additionalProperties": false, "properties": { @@ -54,6 +55,11 @@ "access_list_id": { "$ref": "../common.json#/properties/access_list_id" }, + "upstream_id": { + "description": "Upstream ID", + "type": "integer", + "minimum": 0 + }, "certificate_id": { "$ref": "../common.json#/properties/certificate_id" }, @@ -148,6 +154,16 @@ "$ref": "./access-list-object.json" } ] + }, + "upstream": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./upstream-object.json" + } + ] } } } diff --git a/backend/schema/components/upstream-object.json b/backend/schema/components/upstream-object.json new file mode 100644 index 000000000..549c1184d --- /dev/null +++ b/backend/schema/components/upstream-object.json @@ -0,0 +1,58 @@ +{ + "type": "object", + "description": "Upstream object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "name", + "scheme", + "servers", + "meta" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "scheme": { + "type": "string", + "enum": ["http", "https"] + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "required": ["host", "port"], + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "weight": { + "type": "string" + } + } + } + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index 5cab6e752..c2bd66aa3 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -44,6 +44,9 @@ "certificate_id": { "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" }, + "upstream_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/upstream_id" + }, "ssl_forced": { "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" }, @@ -129,7 +132,8 @@ "roles": ["admin"] }, "certificate": null, - "access_list": null + "access_list": null, + "upstream_id": 0 } } }, diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 85455fb6b..84c6b8b67 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -15,7 +15,7 @@ "schema": { "type": "object", "additionalProperties": false, - "required": ["domain_names", "forward_scheme", "forward_host", "forward_port"], + "required": ["domain_names"], "properties": { "domain_names": { "$ref": "../../../components/proxy-host-object.json#/properties/domain_names" @@ -32,6 +32,9 @@ "certificate_id": { "$ref": "../../../components/proxy-host-object.json#/properties/certificate_id" }, + "upstream_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/upstream_id" + }, "ssl_forced": { "$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced" }, @@ -67,8 +70,33 @@ }, "locations": { "$ref": "../../../components/proxy-host-object.json#/properties/locations" + }, + "forward_to_type": { + "type": "string", + "enum": [ + "host", + "upstream" + ] } - } + }, + "oneOf": [ + { + "required": [ + "forward_host", + "forward_port" + ] + }, + { + "required": [ + "upstream_id" + ], + "properties": { + "upstream_id": { + "minimum": 1 + } + } + } + ] } } } diff --git a/backend/setup.js b/backend/setup.js index 29208a0da..010b2bd7f 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -57,6 +57,7 @@ const setupDefaultUser = () => { streams: 'manage', access_lists: 'manage', certificates: 'manage', + upstreams: 'manage' }); }); }) diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d23ca46fa..087341ce0 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -4,10 +4,25 @@ {% include "_hsts_map.conf" %} +{% if upstream and upstream_id > 0 -%} +# UPSTREAM: {{ upstream.name }} +upstream upstream-{{ id }} { +{% for server in upstream.servers -%} + server {{ server.host }}:{{ server.port }}{{ server.weight | default: "" | prepend: " " }}; +{% endfor -%} +} +{% endif -%} + server { +{% if upstream and upstream_id > 0 -%} + set $forward_scheme {{ upstream.scheme }}; + set $server "upstream-{{ id }}"; + set $port ""; +{% else -%} set $forward_scheme {{ forward_scheme }}; set $server "{{ forward_host }}"; set $port {{ forward_port }}; +{% endif -%} {% include "_listen.conf" %} {% include "_certificates.conf" %} @@ -43,7 +58,12 @@ proxy_http_version 1.1; {% endif %} # Proxy! - include conf.d/include/proxy.conf; + {% if upstream and upstream_id > 0 -%} + include conf.d/include/loadbalancer-proxy.conf; + {% else -%} + include conf.d/include/proxy.conf; + {% endif -%} + } {% endif %} diff --git a/docker/rootfs/etc/nginx/conf.d/include/loadbalancer-proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/loadbalancer-proxy.conf new file mode 100644 index 000000000..9e3fde2ad --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/loadbalancer-proxy.conf @@ -0,0 +1,8 @@ +add_header X-Served-By $host; +proxy_set_header Host $host; +proxy_set_header X-Forwarded-Scheme $scheme; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Real-IP $remote_addr; +proxy_pass $forward_scheme://$server$request_uri; + diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index 6e33a6dca..c64af9d42 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -702,6 +702,43 @@ module.exports = { download: function (id) { return DownloadFile('get', "nginx/certificates/" + id + "/download", "certificate.zip") } + }, + + Upstreams: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/upstreams', expand, query); + }, + + /** + * @param {Object} data + */ + create: function (data) { + return fetch('post', 'nginx/upstreams', data); + }, + + /** + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'nginx/upstreams/' + id, data); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + delete: function (id) { + return fetch('delete', 'nginx/upstreams/' + id); + } } }, diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js index ebddd7807..489d9e236 100644 --- a/frontend/js/app/controller.js +++ b/frontend/js/app/controller.js @@ -368,9 +368,48 @@ module.exports = { showNginxCertificateTestReachability: function (model) { if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { require(['./main', './nginx/certificates/test'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); + App.UI.showModalDialog(new View({model: model})); }); - } + } + }, + + /** + * Nginx Upstreams + */ + showNginxUpstreams: function () { + if (Cache.User.isAdmin() || Cache.User.canView('upstreams')) { + const controller = this; + require(['./main', './nginx/upstreams/main'], (App, View) => { + controller.navigate('/nginx/upstreams'); + App.UI.showAppContent(new View()); + }); + } + }, + + /** + * Nginx Upstream Form + * + * @param [model] + */ + showNginxUpstreamForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('upstreams')) { + require(['./main', './nginx/upstreams/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + + /** + * Upstream Delete Confirm + * + * @param model + */ + showNginxUpstreamDeleteConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('upstreams')) { + require(['./main', './nginx/upstreams/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } }, /** diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 8e7a2a2df..a1e1f5ee8 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -33,7 +33,16 @@ -