diff --git a/.version b/.version index 371a952d6..e4643748f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.12.2 +2.12.6 diff --git a/Jenkinsfile b/Jenkinsfile index 224138bf4..af913c2e0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -128,7 +128,7 @@ pipeline { sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } unstable { - dir(path: 'testing/results') { + dir(path: 'test/results') { archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } } @@ -161,7 +161,7 @@ pipeline { sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } unstable { - dir(path: 'testing/results') { + dir(path: 'test/results') { archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } } @@ -199,7 +199,7 @@ pipeline { sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } unstable { - dir(path: 'testing/results') { + dir(path: 'test/results') { archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') } } @@ -241,12 +241,17 @@ pipeline { } steps { script { - npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on -[DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev) -as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}` + npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): +``` +nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} +``` -**Note:** ensure you backup your NPM instance before testing this image! Especially if there are database changes -**Note:** this is a different docker image namespace than the official image +> [!NOTE] +> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. +> This is a different docker image namespace than the official image. + +> [!WARNING] +> Changes and additions to DNS Providers require verification by at least 2 members of the community! """, true) } } diff --git a/README.md b/README.md index 0e46f00f1..2116a55ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-
+
diff --git a/backend/index.js b/backend/index.js
index 551378251..d334a7c2d 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -3,6 +3,8 @@
const schema = require('./schema');
const logger = require('./logger').global;
+const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false';
+
async function appStart () {
const migrate = require('./migrate');
const setup = require('./setup');
@@ -13,7 +15,16 @@ async function appStart () {
return migrate.latest()
.then(setup)
.then(schema.getCompiledSchema)
- .then(internalIpRanges.fetch)
+ .then(() => {
+ if (IP_RANGES_FETCH_ENABLED) {
+ logger.info('IP Ranges fetch is enabled');
+ return internalIpRanges.fetch().catch((err) => {
+ logger.error('IP Ranges fetch failed, continuing anyway:', err.message);
+ });
+ } else {
+ logger.info('IP Ranges fetch is disabled by environment variable');
+ }
+ })
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js
index 41c975e6e..f6043e18b 100644
--- a/backend/internal/access-list.js
+++ b/backend/internal/access-list.js
@@ -258,6 +258,7 @@ const internalAccessList = {
})
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
+ .groupBy('access_list.id')
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();
@@ -507,8 +508,13 @@ const internalAccessList = {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
- utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
- .then((/*result*/) => {
+ utils.execFile('openssl', ['passwd', '-apr1', item.password])
+ .then((res) => {
+ try {
+ fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'});
+ } catch (err) {
+ reject(err);
+ }
next();
})
.catch((err) => {
diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js
index 34b8fdf5a..f2e845a24 100644
--- a/backend/internal/certificate.js
+++ b/backend/internal/certificate.js
@@ -313,6 +313,9 @@ const internalCertificate = {
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
+ .allowGraph('[proxy_hosts]')
+ .allowGraph('[redirection_hosts]')
+ .allowGraph('[dead_hosts]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -464,6 +467,9 @@ const internalCertificate = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
+ .allowGraph('[proxy_hosts]')
+ .allowGraph('[redirection_hosts]')
+ .allowGraph('[dead_hosts]')
.orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') {
diff --git a/backend/internal/stream.js b/backend/internal/stream.js
index 9f76a1def..50ce08324 100644
--- a/backend/internal/stream.js
+++ b/backend/internal/stream.js
@@ -1,13 +1,15 @@
-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'];
+ return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted'];
}
const internalStream = {
@@ -18,6 +20,12 @@ const internalStream = {
* @returns {Promise}
*/
create: (access, data) => {
+ const 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,16 +35,44 @@ const internalStream = {
data.meta = {};
}
+ // streams aren't routed by domain name so don't store domain names in the DB
+ let data_no_domains = structuredClone(data);
+ 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)
.then(() => {
- return internalStream.get(access, {id: row.id, expand: ['owner']});
+ return row;
});
})
.then((row) => {
@@ -60,6 +96,12 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
+ const 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,16 +113,32 @@ 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)
.then(utils.omitRow(omissions()))
- .then((saved_row) => {
- return internalNginx.configure(streamModel, 'stream', saved_row)
- .then(() => {
- return internalStream.get(access, {id: row.id, expand: ['owner']});
- });
- })
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
@@ -93,6 +151,17 @@ const internalStream = {
return saved_row;
});
});
+ })
+ .then(() => {
+ return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
+ .then((row) => {
+ return internalNginx.configure(streamModel, 'stream', row)
+ .then((new_meta) => {
+ row.meta = new_meta;
+ row = internalHost.cleanRowCertificateMeta(row);
+ return _.omit(row, omissions());
+ });
+ });
});
},
@@ -115,7 +184,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 +201,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 +267,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 +320,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,8 +368,8 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
- .allowGraph('[owner]')
- .orderByRaw('CAST(incoming_port AS INTEGER) ASC');
+ .allowGraph('[owner,certificate]')
+ .orderBy('incoming_port', 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
@@ -317,6 +387,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/lib/certbot.js b/backend/lib/certbot.js
index eb1966dc7..96d947102 100644
--- a/backend/lib/certbot.js
+++ b/backend/lib/certbot.js
@@ -11,7 +11,7 @@ const certbot = {
/**
* @param {array} pluginKeys
*/
- installPlugins: async function (pluginKeys) {
+ installPlugins: async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
@@ -21,7 +21,7 @@ const certbot = {
}
batchflow(pluginKeys).sequential()
- .each((i, pluginKey, next) => {
+ .each((_i, pluginKey, next) => {
certbot.installPlugin(pluginKey)
.then(() => {
next();
@@ -51,7 +51,7 @@ const certbot = {
* @param {string} pluginKey
* @returns {Object}
*/
- installPlugin: async function (pluginKey) {
+ installPlugin: async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.ItemNotFoundError(pluginKey);
@@ -63,8 +63,15 @@ const certbot = {
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
- const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
- return utils.exec(cmd)
+ // SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
+ // in new versions of Python
+ let env = Object.assign({}, process.env, {SETUPTOOLS_USE_DISTUTILS: 'stdlib'});
+ if (typeof plugin.env === 'object') {
+ env = Object.assign(env, plugin.env);
+ }
+
+ const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
+ return utils.exec(cmd, {env})
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
diff --git a/backend/lib/utils.js b/backend/lib/utils.js
index bcdb3341c..66f2dfd95 100644
--- a/backend/lib/utils.js
+++ b/backend/lib/utils.js
@@ -1,13 +1,13 @@
const _ = require('lodash');
-const exec = require('child_process').exec;
-const execFile = require('child_process').execFile;
+const exec = require('node:child_process').exec;
+const execFile = require('node:child_process').execFile;
const { Liquid } = require('liquidjs');
const logger = require('../logger').global;
const error = require('./error');
module.exports = {
- exec: async function(cmd, options = {}) {
+ exec: async (cmd, options = {}) => {
logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
@@ -31,11 +31,11 @@ module.exports = {
* @param {Array} args
* @returns {Promise}
*/
- execFile: function (cmd, args) {
+ execFile: (cmd, args) => {
// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
return new Promise((resolve, reject) => {
- execFile(cmd, args, function (err, stdout, /*stderr*/) {
+ execFile(cmd, args, (err, stdout, /*stderr*/) => {
if (err && typeof err === 'object') {
reject(err);
} else {
@@ -51,7 +51,7 @@ module.exports = {
* @param {Array} omissions
* @returns {Function}
*/
- omitRow: function (omissions) {
+ omitRow: (omissions) => {
/**
* @param {Object} row
* @returns {Object}
@@ -67,7 +67,7 @@ module.exports = {
* @param {Array} omissions
* @returns {Function}
*/
- omitRows: function (omissions) {
+ omitRows: (omissions) => {
/**
* @param {Array} rows
* @returns {Object}
@@ -83,9 +83,9 @@ module.exports = {
/**
* @returns {Object} Liquid render engine
*/
- getRenderEngine: function () {
+ getRenderEngine: () => {
const renderEngine = new Liquid({
- root: __dirname + '/../templates/'
+ root: `${__dirname}/../templates/`
});
/**
diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js
new file mode 100644
index 000000000..5f47b18ec
--- /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/certificate.js b/backend/models/certificate.js
index 534d927cb..d4ea21ad5 100644
--- a/backend/models/certificate.js
+++ b/backend/models/certificate.js
@@ -4,7 +4,6 @@
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);
@@ -68,6 +67,11 @@ class Certificate extends Model {
}
static get relationMappings () {
+ const ProxyHost = require('./proxy_host');
+ const DeadHost = require('./dead_host');
+ const User = require('./user');
+ const RedirectionHost = require('./redirection_host');
+
return {
owner: {
relation: Model.HasOneRelation,
@@ -79,6 +83,39 @@ class Certificate extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
+ },
+ proxy_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: ProxyHost,
+ join: {
+ from: 'certificate.id',
+ to: 'proxy_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('proxy_host.is_deleted', 0);
+ }
+ },
+ dead_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: DeadHost,
+ join: {
+ from: 'certificate.id',
+ to: 'dead_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('dead_host.is_deleted', 0);
+ }
+ },
+ redirection_hosts: {
+ relation: Model.HasManyRelation,
+ modelClass: RedirectionHost,
+ join: {
+ from: 'certificate.id',
+ to: 'redirection_host.certificate_id'
+ },
+ modify: function (qb) {
+ qb.where('redirection_host.is_deleted', 0);
+ }
}
};
}
diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js
index 483da3b6b..3386caabf 100644
--- a/backend/models/dead_host.js
+++ b/backend/models/dead_host.js
@@ -12,7 +12,11 @@ Model.knex(db);
const boolFields = [
'is_deleted',
+ 'ssl_forced',
+ 'http2_support',
'enabled',
+ 'hsts_enabled',
+ 'hsts_subdomains',
];
class DeadHost extends Model {
diff --git a/backend/models/stream.js b/backend/models/stream.js
index b96ca5a17..5d1cb6c1c 100644
--- a/backend/models/stream.js
+++ b/backend/models/stream.js
@@ -1,16 +1,15 @@
-// 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);
const boolFields = [
'is_deleted',
+ 'enabled',
'tcp_forwarding',
'udp_forwarding',
];
@@ -64,6 +63,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/backend/routes/users.js b/backend/routes/users.js
index f8ce366c9..e41bf6cfb 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -181,7 +181,7 @@ router
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
@@ -212,7 +212,7 @@ router
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
@@ -238,7 +238,7 @@ router
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
- res.status(201)
+ res.status(200)
.send(result);
})
.catch(next);
diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json
index 5098802b1..e9dcacb5e 100644
--- a/backend/schema/components/proxy-host-object.json
+++ b/backend/schema/components/proxy-host-object.json
@@ -22,8 +22,7 @@
"enabled",
"locations",
"hsts_enabled",
- "hsts_subdomains",
- "certificate"
+ "hsts_subdomains"
],
"additionalProperties": false,
"properties": {
diff --git a/backend/schema/components/stream-list.json b/backend/schema/components/stream-list.json
index 39789b4a7..b6e8b6d44 100644
--- a/backend/schema/components/stream-list.json
+++ b/backend/schema/components/stream-list.json
@@ -1,7 +1,7 @@
{
"type": "array",
- "description": "Proxy Hosts list",
+ "description": "Streams list",
"items": {
- "$ref": "./proxy-host-object.json"
+ "$ref": "./stream-object.json"
}
}
diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json
index e17749940..848c30e6e 100644
--- a/backend/schema/components/stream-object.json
+++ b/backend/schema/components/stream-object.json
@@ -19,9 +19,7 @@
"incoming_port": {
"type": "integer",
"minimum": 1,
- "maximum": 65535,
- "if": {"properties": {"tcp_forwarding": {"const": true}}},
- "then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
+ "maximum": 65535
},
"forwarding_host": {
"anyOf": [
@@ -55,8 +53,24 @@
"enabled": {
"$ref": "../common.json#/properties/enabled"
},
+ "certificate_id": {
+ "$ref": "../common.json#/properties/certificate_id"
+ },
"meta": {
"type": "object"
+ },
+ "owner": {
+ "$ref": "./user-object.json"
+ },
+ "certificate": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "./certificate-object.json"
+ }
+ ]
}
}
}
diff --git a/backend/schema/components/token-object.json b/backend/schema/components/token-object.json
index a7044bce9..6ec4e4348 100644
--- a/backend/schema/components/token-object.json
+++ b/backend/schema/components/token-object.json
@@ -5,10 +5,9 @@
"additionalProperties": false,
"properties": {
"expires": {
- "description": "Token Expiry Unix Time",
- "example": 1566540249,
- "minimum": 1,
- "type": "number"
+ "description": "Token Expiry ISO Time String",
+ "example": "2025-02-04T20:40:46.340Z",
+ "type": "string"
},
"token": {
"description": "JWT Token",
diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json
index 596afc6e7..17969ee4e 100644
--- a/backend/schema/paths/nginx/streams/get.json
+++ b/backend/schema/paths/nginx/streams/get.json
@@ -14,7 +14,7 @@
"description": "Expansions",
"schema": {
"type": "string",
- "enum": ["access_list", "owner", "certificate"]
+ "enum": ["owner", "certificate"]
}
}
],
@@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
- "enabled": true
+ "enabled": true,
+ "certificate_id": 0
}
]
}
diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json
index 9f3514e0f..d26996b69 100644
--- a/backend/schema/paths/nginx/streams/post.json
+++ b/backend/schema/paths/nginx/streams/post.json
@@ -32,6 +32,9 @@
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
+ "certificate_id": {
+ "$ref": "../../../components/stream-object.json#/properties/certificate_id"
+ },
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
}
@@ -73,7 +76,8 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
- }
+ },
+ "certificate_id": 0
}
}
},
diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json
index 6547656df..801af13a7 100644
--- a/backend/schema/paths/nginx/streams/streamID/get.json
+++ b/backend/schema/paths/nginx/streams/streamID/get.json
@@ -40,7 +40,8 @@
"nginx_online": true,
"nginx_err": null
},
- "enabled": true
+ "enabled": true,
+ "certificate_id": 0
}
}
},
diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json
index fbfdc901b..14adb1631 100644
--- a/backend/schema/paths/nginx/streams/streamID/put.json
+++ b/backend/schema/paths/nginx/streams/streamID/put.json
@@ -29,56 +29,26 @@
"additionalProperties": false,
"minProperties": 1,
"properties": {
- "domain_names": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
+ "incoming_port": {
+ "$ref": "../../../../components/stream-object.json#/properties/incoming_port"
},
- "forward_scheme": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
+ "forwarding_host": {
+ "$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
},
- "forward_host": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
+ "forwarding_port": {
+ "$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
},
- "forward_port": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
+ "tcp_forwarding": {
+ "$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
},
- "certificate_id": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id"
- },
- "ssl_forced": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced"
- },
- "hsts_enabled": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled"
- },
- "hsts_subdomains": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
- },
- "http2_support": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
- },
- "block_exploits": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits"
+ "udp_forwarding": {
+ "$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
},
- "caching_enabled": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
- },
- "allow_websocket_upgrade": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
- },
- "access_list_id": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
- },
- "advanced_config": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config"
- },
- "enabled": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/enabled"
+ "certificate_id": {
+ "$ref": "../../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/meta"
- },
- "locations": {
- "$ref": "../../../../components/proxy-host-object.json#/properties/locations"
+ "$ref": "../../../../components/stream-object.json#/properties/meta"
}
}
}
@@ -94,42 +64,32 @@
"default": {
"value": {
"id": 1,
- "created_on": "2024-10-08T23:23:03.000Z",
- "modified_on": "2024-10-08T23:26:37.000Z",
+ "created_on": "2024-10-09T02:33:45.000Z",
+ "modified_on": "2024-10-09T02:33:45.000Z",
"owner_user_id": 1,
- "domain_names": ["test.example.com"],
- "forward_host": "192.168.0.10",
- "forward_port": 8989,
- "access_list_id": 0,
- "certificate_id": 0,
- "ssl_forced": false,
- "caching_enabled": false,
- "block_exploits": false,
- "advanced_config": "",
+ "incoming_port": 9090,
+ "forwarding_host": "router.internal",
+ "forwarding_port": 80,
+ "tcp_forwarding": true,
+ "udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
- "allow_websocket_upgrade": false,
- "http2_support": false,
- "forward_scheme": "http",
"enabled": true,
- "hsts_enabled": false,
- "hsts_subdomains": false,
"owner": {
"id": 1,
- "created_on": "2024-10-07T22:43:55.000Z",
- "modified_on": "2024-10-08T12:52:54.000Z",
+ "created_on": "2024-10-09T02:33:16.000Z",
+ "modified_on": "2024-10-09T02:33:16.000Z",
"is_deleted": false,
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
- "nickname": "some guy",
- "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
+ "nickname": "Admin",
+ "avatar": "",
"roles": ["admin"]
},
- "certificate": null,
- "access_list": null
+ "certificate_id": 0
}
}
},
diff --git a/backend/schema/paths/tokens/get.json b/backend/schema/paths/tokens/get.json
index 859bc61a4..ef842eafe 100644
--- a/backend/schema/paths/tokens/get.json
+++ b/backend/schema/paths/tokens/get.json
@@ -15,7 +15,7 @@
"examples": {
"default": {
"value": {
- "expires": 1566540510,
+ "expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
diff --git a/backend/schema/paths/tokens/post.json b/backend/schema/paths/tokens/post.json
index dece6b656..99703ff0d 100644
--- a/backend/schema/paths/tokens/post.json
+++ b/backend/schema/paths/tokens/post.json
@@ -38,7 +38,7 @@
"default": {
"value": {
"result": {
- "expires": 1566540510,
+ "expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json
index 5a0142bff..4a502b4e4 100644
--- a/backend/schema/swagger.json
+++ b/backend/schema/swagger.json
@@ -9,6 +9,15 @@
"url": "http://127.0.0.1:81/api"
}
],
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ }
+ },
"paths": {
"/": {
"get": {
diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf
index 06ca7bb87..efcca5cd5 100644
--- a/backend/templates/_certificates.conf
+++ b/backend/templates/_certificates.conf
@@ -2,6 +2,7 @@
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
+ include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
diff --git a/backend/templates/_certificates_stream.conf b/backend/templates/_certificates_stream.conf
new file mode 100644
index 000000000..ba7812fdd
--- /dev/null
+++ b/backend/templates/_certificates_stream.conf
@@ -0,0 +1,13 @@
+{% if certificate and certificate_id > 0 %}
+{% if certificate.provider == "letsencrypt" %}
+ # Let's Encrypt SSL
+ include conf.d/include/ssl-cache-stream.conf;
+ include conf.d/include/ssl-ciphers.conf;
+ ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
+{%- else %}
+ # Custom SSL
+ ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
+ ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
+{%- endif -%}
+{%- endif -%}
diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf
index 76159a646..7333aaee1 100644
--- a/backend/templates/stream.conf
+++ b/backend/templates/stream.conf
@@ -5,12 +5,10 @@
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
- listen {{ incoming_port }};
-{% if ipv6 -%}
- listen [::]:{{ incoming_port }};
-{% else -%}
- #listen [::]:{{ incoming_port }};
-{% endif %}
+ listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
+ {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
+
+ {%- include "_certificates_stream.conf" %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
@@ -19,14 +17,12 @@ server {
include /data/nginx/custom/server_stream_tcp[.]conf;
}
{% endif %}
-{% if udp_forwarding == 1 or udp_forwarding == true %}
+
+{% if udp_forwarding == 1 or udp_forwarding == true -%}
server {
listen {{ incoming_port }} udp;
-{% if ipv6 -%}
- listen [::]:{{ incoming_port }} udp;
-{% else -%}
- #listen [::]:{{ incoming_port }} udp;
-{% endif %}
+ {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
+
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom
diff --git a/backend/yarn.lock b/backend/yarn.lock
index cea8210bc..bae734b43 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -492,9 +492,9 @@ boxen@^4.2.0:
widest-line "^3.1.0"
brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
diff --git a/docker/docker-compose.ci.mysql.yml b/docker/docker-compose.ci.mysql.yml
index 388cdb382..108a1dca3 100644
--- a/docker/docker-compose.ci.mysql.yml
+++ b/docker/docker-compose.ci.mysql.yml
@@ -18,6 +18,7 @@ services:
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npmpass'
+ MARIADB_AUTO_UPGRADE: '1'
volumes:
- mysql_vol:/var/lib/mysql
networks:
diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml
index bb68858f9..280a05465 100644
--- a/docker/docker-compose.ci.yml
+++ b/docker/docker-compose.ci.yml
@@ -22,6 +22,10 @@ services:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
+ expose:
+ - '80-81/tcp'
+ - '443/tcp'
+ - '1500-1503/tcp'
networks:
fulltest:
aliases:
@@ -40,7 +44,7 @@ services:
- ca.internal
pdns:
- image: pschiffe/pdns-mysql
+ image: pschiffe/pdns-mysql:4.8
volumes:
- '/etc/localtime:/etc/localtime:ro'
environment:
@@ -97,7 +101,7 @@ services:
HTTP_PROXY: 'squid:3128'
HTTPS_PROXY: 'squid:3128'
volumes:
- - 'cypress_logs:/results'
+ - 'cypress_logs:/test/results'
- './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro'
command: cypress run --browser chrome --config-file=cypress/config/ci.js
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 50ca55537..5abe057b0 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -132,7 +132,7 @@ services:
- 8128:3128
pdns:
- image: pschiffe/pdns-mysql
+ image: pschiffe/pdns-mysql:4.8
container_name: npm2dev.pdns
volumes:
- '/etc/localtime:/etc/localtime:ro'
@@ -218,7 +218,7 @@ services:
env_file:
- ci.env
ports:
- - 9000:9000
+ - 9000:9000
depends_on:
- authentik-redis
- db-postgres
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf
new file mode 100644
index 000000000..433555dfa
--- /dev/null
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf
@@ -0,0 +1,2 @@
+ssl_session_timeout 5m;
+ssl_session_cache shared:SSL_stream:50m;
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
new file mode 100644
index 000000000..aa7ba2cb7
--- /dev/null
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf
@@ -0,0 +1,2 @@
+ssl_session_timeout 5m;
+ssl_session_cache shared:SSL:50m;
diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
index 233abb6e9..b5dacfb57 100644
--- a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
+++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf
@@ -1,6 +1,3 @@
-ssl_session_timeout 5m;
-ssl_session_cache shared:SSL:50m;
-
# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
index 378cc9caa..fa9465189 100755
--- a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
@@ -8,21 +8,53 @@ log_info 'Setting ownership ...'
# root
chown root /tmp/nginx
-# npm user and group
-chown -R "$PUID:$PGID" /data
-chown -R "$PUID:$PGID" /etc/letsencrypt
-chown -R "$PUID:$PGID" /run/nginx
-chown -R "$PUID:$PGID" /tmp/nginx
-chown -R "$PUID:$PGID" /var/cache/nginx
-chown -R "$PUID:$PGID" /var/lib/logrotate
-chown -R "$PUID:$PGID" /var/lib/nginx
-chown -R "$PUID:$PGID" /var/log/nginx
-
-# Don't chown entire /etc/nginx folder as this causes crashes on some systems
-chown -R "$PUID:$PGID" /etc/nginx/nginx
-chown -R "$PUID:$PGID" /etc/nginx/nginx.conf
-chown -R "$PUID:$PGID" /etc/nginx/conf.d
-
-# Prevents errors when installing python certbot plugins when non-root
-chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
-find /opt/certbot/lib/python*/site-packages -not -user "$PUID" -execdir chown "$PUID:$PGID" {} \+
+locations=(
+ "/data"
+ "/etc/letsencrypt"
+ "/run/nginx"
+ "/tmp/nginx"
+ "/var/cache/nginx"
+ "/var/lib/logrotate"
+ "/var/lib/nginx"
+ "/var/log/nginx"
+ "/etc/nginx/nginx"
+ "/etc/nginx/nginx.conf"
+ "/etc/nginx/conf.d"
+)
+
+chownit() {
+ local dir="$1"
+ local recursive="${2:-true}"
+
+ local have
+ have="$(stat -c '%u:%g' "$dir")"
+ echo "- $dir ... "
+
+ if [ "$have" != "$PUID:$PGID" ]; then
+ if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then
+ chown -R "$PUID:$PGID" "$dir"
+ else
+ chown "$PUID:$PGID" "$dir"
+ fi
+ echo " DONE"
+ else
+ echo " SKIPPED"
+ fi
+}
+
+for loc in "${locations[@]}"; do
+ chownit "$loc"
+done
+
+if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then
+ log_info 'Skipping ownership change of certbot directories'
+else
+ log_info 'Changing ownership of certbot directories, this may take some time ...'
+ chownit "/opt/certbot" false
+ chownit "/opt/certbot/bin" false
+
+ # Handle all site-packages directories efficiently
+ find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do
+ chownit "$SITE_PACKAGES_DIR"
+ done
+fi
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
index 0cb9f1264..e02f41ca1 100755
--- a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
@@ -5,12 +5,9 @@ set -e
log_info 'Dynamic resolvers ...'
-DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
-
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm
-if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ];
-then
+if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
index 0c4d261ce..2ae61ae55 100755
--- a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
@@ -8,14 +8,11 @@ set -e
log_info 'IPv6 ...'
-# Lowercase
-DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
-
process_folder () {
FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX=
- if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
+ if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
diff --git a/docker/rootfs/usr/bin/common.sh b/docker/rootfs/usr/bin/common.sh
index 13cf06acd..46529870a 100644
--- a/docker/rootfs/usr/bin/common.sh
+++ b/docker/rootfs/usr/bin/common.sh
@@ -56,3 +56,13 @@ get_group_id () {
getent group "$1" | cut -d: -f3
fi
}
+
+# param $1: value
+is_true () {
+ VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')
+ if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then
+ echo '1'
+ else
+ echo '0'
+ fi
+}
diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6
index 2922735b2..639c65dd6 100755
--- a/docker/scripts/install-s6
+++ b/docker/scripts/install-s6
@@ -8,7 +8,7 @@ BLUE='\E[1;34m'
GREEN='\E[1;32m'
RESET='\E[0m'
-S6_OVERLAY_VERSION=3.1.5.0
+S6_OVERLAY_VERSION=3.2.1.0
TARGETPLATFORM=${1:-linux/amd64}
# Determine the correct binary file for the architecture given
diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md
index 373fd08bb..4a7c260eb 100644
--- a/docs/src/advanced-config/index.md
+++ b/docs/src/advanced-config/index.md
@@ -161,6 +161,14 @@ The easy fix is to add a Docker environment variable to the Nginx Proxy Manager
DISABLE_IPV6: 'true'
```
+## Disabling IP Ranges Fetch
+
+By default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled:
+
+```yml
+ environment:
+ IP_RANGES_FETCH_ENABLED: 'false'
+```
## Custom Nginx Configurations
diff --git a/docs/src/setup/index.md b/docs/src/setup/index.md
index 5e126754f..c2296da7f 100644
--- a/docs/src/setup/index.md
+++ b/docs/src/setup/index.md
@@ -21,7 +21,7 @@ services:
# Add any other Stream port you want to expose
# - '21:21' # FTP
- environment:
+ #environment:
# Uncomment this if you want to change the location of
# the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite"
diff --git a/docs/yarn.lock b/docs/yarn.lock
index f89b80fdf..c95905f68 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -1065,9 +1065,9 @@ vfile@^6.0.0:
vfile-message "^4.0.0"
vite@^5.4.8:
- version "5.4.8"
- resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8"
- integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
+ version "5.4.19"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
+ integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js
index ccb2978a8..ebddd7807 100644
--- a/frontend/js/app/controller.js
+++ b/frontend/js/app/controller.js
@@ -4,444 +4,438 @@ const Tokens = require('./tokens');
module.exports = {
- /**
- * @param {String} route
- * @param {Object} [options]
- * @returns {Boolean}
- */
- navigate: function (route, options) {
- options = options || {};
- Backbone.history.navigate(route.toString(), options);
- return true;
- },
-
- /**
- * Login
- */
- showLogin: function () {
- window.location = '/login';
- },
-
- /**
- * Users
- */
- showUsers: function () {
- let controller = this;
- if (Cache.User.isAdmin()) {
- require(['./main', './users/main'], (App, View) => {
- controller.navigate('/users');
- App.UI.showAppContent(new View());
- });
- } else {
- this.showDashboard();
- }
- },
-
- /**
- * User Form
- *
- * @param [model]
- */
- showUserForm: function (model) {
- if (Cache.User.isAdmin()) {
- require(['./main', './user/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * User Permissions Form
- *
- * @param model
- */
- showUserPermissions: function (model) {
- if (Cache.User.isAdmin()) {
- require(['./main', './user/permissions'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * User Password Form
- *
- * @param model
- */
- showUserPasswordForm: function (model) {
- if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
- require(['./main', './user/password'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * User Delete Confirm
- *
- * @param model
- */
- showUserDeleteConfirm: function (model) {
- if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
- require(['./main', './user/delete'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Dashboard
- */
- showDashboard: function () {
- let controller = this;
-
- require(['./main', './dashboard/main'], (App, View) => {
- controller.navigate('/');
- App.UI.showAppContent(new View());
- });
- },
-
- /**
- * Nginx Proxy Hosts
- */
- showNginxProxy: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
- let controller = this;
-
- require(['./main', './nginx/proxy/main'], (App, View) => {
- controller.navigate('/nginx/proxy');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * Nginx Proxy Host Form
- *
- * @param [model]
- */
- showNginxProxyForm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
- require(['./main', './nginx/proxy/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Proxy Host Delete Confirm
- *
- * @param model
- */
- showNginxProxyDeleteConfirm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
- require(['./main', './nginx/proxy/delete'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Nginx Redirection Hosts
- */
- showNginxRedirection: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
- let controller = this;
-
- require(['./main', './nginx/redirection/main'], (App, View) => {
- controller.navigate('/nginx/redirection');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * Nginx Redirection Host Form
- *
- * @param [model]
- */
- showNginxRedirectionForm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
- require(['./main', './nginx/redirection/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Proxy Redirection Delete Confirm
- *
- * @param model
- */
- showNginxRedirectionDeleteConfirm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
- require(['./main', './nginx/redirection/delete'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Nginx Stream Hosts
- */
- showNginxStream: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
- let controller = this;
-
- require(['./main', './nginx/stream/main'], (App, View) => {
- controller.navigate('/nginx/stream');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * Stream Form
- *
- * @param [model]
- */
- showNginxStreamForm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
- require(['./main', './nginx/stream/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Stream Delete Confirm
- *
- * @param model
- */
- showNginxStreamDeleteConfirm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
- require(['./main', './nginx/stream/delete'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Nginx Dead Hosts
- */
- showNginxDead: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
- let controller = this;
-
- require(['./main', './nginx/dead/main'], (App, View) => {
- controller.navigate('/nginx/404');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * Dead Host Form
- *
- * @param [model]
- */
- showNginxDeadForm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
- require(['./main', './nginx/dead/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Dead Host Delete Confirm
- *
- * @param model
- */
- showNginxDeadDeleteConfirm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
- require(['./main', './nginx/dead/delete'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Help Dialog
- *
- * @param {String} title
- * @param {String} content
- */
- showHelp: function (title, content) {
- require(['./main', './help/main'], function (App, View) {
- App.UI.showModalDialog(new View({title: title, content: content}));
- });
- },
-
- /**
- * Nginx Access
- */
- showNginxAccess: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
- let controller = this;
-
- require(['./main', './nginx/access/main'], (App, View) => {
- controller.navigate('/nginx/access');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * Nginx Access List Form
- *
- * @param [model]
- */
- showNginxAccessListForm: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
- require(['./main', './nginx/access/form'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * 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
- */
- showNginxCertificates: function () {
- if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
- let controller = this;
-
- require(['./main', './nginx/certificates/main'], (App, View) => {
- controller.navigate('/nginx/certificates');
- App.UI.showAppContent(new View());
- });
- }
- },
-
- /**
- * 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 Renew
- *
- * @param model
- */
- showNginxCertificateRenew: function (model) {
- if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
- require(['./main', './nginx/certificates/renew'], 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}));
- });
- }
- },
-
- /**
- * Certificate Test Reachability
- *
- * @param model
- */
- 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}));
- });
- }
- },
-
- /**
- * Audit Log
- */
- showAuditLog: function () {
- let controller = this;
- if (Cache.User.isAdmin()) {
- require(['./main', './audit-log/main'], (App, View) => {
- controller.navigate('/audit-log');
- App.UI.showAppContent(new View());
- });
- } else {
- this.showDashboard();
- }
- },
-
- /**
- * Audit Log Metadata
- *
- * @param model
- */
- showAuditMeta: function (model) {
- if (Cache.User.isAdmin()) {
- require(['./main', './audit-log/meta'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- },
-
- /**
- * Settings
- */
- showSettings: function () {
- let controller = this;
- if (Cache.User.isAdmin()) {
- require(['./main', './settings/main'], (App, View) => {
- controller.navigate('/settings');
- App.UI.showAppContent(new View());
- });
- } else {
- this.showDashboard();
- }
- },
-
- /**
- * Settings Item Form
- *
- * @param model
- */
- showSettingForm: function (model) {
- if (Cache.User.isAdmin()) {
- if (model.get('id') === 'default-site') {
- require(['./main', './settings/default-site/main'], function (App, View) {
- App.UI.showModalDialog(new View({model: model}));
- });
- }
- }
- },
-
- /**
- * Logout
- */
- logout: function () {
- Tokens.dropTopToken();
- this.showLogin();
- }
+ /**
+ * @param {String} route
+ * @param {Object} [options]
+ * @returns {Boolean}
+ */
+ navigate: function (route, options) {
+ options = options || {};
+ Backbone.history.navigate(route.toString(), options);
+ return true;
+ },
+
+ /**
+ * Login
+ */
+ showLogin: function () {
+ window.location = '/login';
+ },
+
+ /**
+ * Users
+ */
+ showUsers: function () {
+ const controller = this;
+ if (Cache.User.isAdmin()) {
+ require(['./main', './users/main'], (App, View) => {
+ controller.navigate('/users');
+ App.UI.showAppContent(new View());
+ });
+ } else {
+ this.showDashboard();
+ }
+ },
+
+ /**
+ * User Form
+ *
+ * @param [model]
+ */
+ showUserForm: function (model) {
+ if (Cache.User.isAdmin()) {
+ require(['./main', './user/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * User Permissions Form
+ *
+ * @param model
+ */
+ showUserPermissions: function (model) {
+ if (Cache.User.isAdmin()) {
+ require(['./main', './user/permissions'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * User Password Form
+ *
+ * @param model
+ */
+ showUserPasswordForm: function (model) {
+ if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) {
+ require(['./main', './user/password'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * User Delete Confirm
+ *
+ * @param model
+ */
+ showUserDeleteConfirm: function (model) {
+ if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) {
+ require(['./main', './user/delete'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Dashboard
+ */
+ showDashboard: function () {
+ const controller = this;
+ require(['./main', './dashboard/main'], (App, View) => {
+ controller.navigate('/');
+ App.UI.showAppContent(new View());
+ });
+ },
+
+ /**
+ * Nginx Proxy Hosts
+ */
+ showNginxProxy: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) {
+ const controller = this;
+
+ require(['./main', './nginx/proxy/main'], (App, View) => {
+ controller.navigate('/nginx/proxy');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * Nginx Proxy Host Form
+ *
+ * @param [model]
+ */
+ showNginxProxyForm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
+ require(['./main', './nginx/proxy/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Proxy Host Delete Confirm
+ *
+ * @param model
+ */
+ showNginxProxyDeleteConfirm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) {
+ require(['./main', './nginx/proxy/delete'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Nginx Redirection Hosts
+ */
+ showNginxRedirection: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) {
+ const controller = this;
+ require(['./main', './nginx/redirection/main'], (App, View) => {
+ controller.navigate('/nginx/redirection');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * Nginx Redirection Host Form
+ *
+ * @param [model]
+ */
+ showNginxRedirectionForm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
+ require(['./main', './nginx/redirection/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Proxy Redirection Delete Confirm
+ *
+ * @param model
+ */
+ showNginxRedirectionDeleteConfirm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) {
+ require(['./main', './nginx/redirection/delete'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Nginx Stream Hosts
+ */
+ showNginxStream: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('streams')) {
+ const controller = this;
+ require(['./main', './nginx/stream/main'], (App, View) => {
+ controller.navigate('/nginx/stream');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * Stream Form
+ *
+ * @param [model]
+ */
+ showNginxStreamForm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
+ require(['./main', './nginx/stream/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Stream Delete Confirm
+ *
+ * @param model
+ */
+ showNginxStreamDeleteConfirm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('streams')) {
+ require(['./main', './nginx/stream/delete'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Nginx Dead Hosts
+ */
+ showNginxDead: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) {
+ const controller = this;
+ require(['./main', './nginx/dead/main'], (App, View) => {
+ controller.navigate('/nginx/404');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * Dead Host Form
+ *
+ * @param [model]
+ */
+ showNginxDeadForm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
+ require(['./main', './nginx/dead/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Dead Host Delete Confirm
+ *
+ * @param model
+ */
+ showNginxDeadDeleteConfirm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) {
+ require(['./main', './nginx/dead/delete'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Help Dialog
+ *
+ * @param {String} title
+ * @param {String} content
+ */
+ showHelp: function (title, content) {
+ require(['./main', './help/main'], function (App, View) {
+ App.UI.showModalDialog(new View({title: title, content: content}));
+ });
+ },
+
+ /**
+ * Nginx Access
+ */
+ showNginxAccess: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) {
+ const controller = this;
+ require(['./main', './nginx/access/main'], (App, View) => {
+ controller.navigate('/nginx/access');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * Nginx Access List Form
+ *
+ * @param [model]
+ */
+ showNginxAccessListForm: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) {
+ require(['./main', './nginx/access/form'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * 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
+ */
+ showNginxCertificates: function () {
+ if (Cache.User.isAdmin() || Cache.User.canView('certificates')) {
+ const controller = this;
+ require(['./main', './nginx/certificates/main'], (App, View) => {
+ controller.navigate('/nginx/certificates');
+ App.UI.showAppContent(new View());
+ });
+ }
+ },
+
+ /**
+ * 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 Renew
+ *
+ * @param model
+ */
+ showNginxCertificateRenew: function (model) {
+ if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
+ require(['./main', './nginx/certificates/renew'], 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}));
+ });
+ }
+ },
+
+ /**
+ * Certificate Test Reachability
+ *
+ * @param model
+ */
+ 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}));
+ });
+ }
+ },
+
+ /**
+ * Audit Log
+ */
+ showAuditLog: function () {
+ const controller = this;
+ if (Cache.User.isAdmin()) {
+ require(['./main', './audit-log/main'], (App, View) => {
+ controller.navigate('/audit-log');
+ App.UI.showAppContent(new View());
+ });
+ } else {
+ this.showDashboard();
+ }
+ },
+
+ /**
+ * Audit Log Metadata
+ *
+ * @param model
+ */
+ showAuditMeta: function (model) {
+ if (Cache.User.isAdmin()) {
+ require(['./main', './audit-log/meta'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ },
+
+ /**
+ * Settings
+ */
+ showSettings: function () {
+ const controller = this;
+ if (Cache.User.isAdmin()) {
+ require(['./main', './settings/main'], (App, View) => {
+ controller.navigate('/settings');
+ App.UI.showAppContent(new View());
+ });
+ } else {
+ this.showDashboard();
+ }
+ },
+
+ /**
+ * Settings Item Form
+ *
+ * @param model
+ */
+ showSettingForm: function (model) {
+ if (Cache.User.isAdmin()) {
+ if (model.get('id') === 'default-site') {
+ require(['./main', './settings/default-site/main'], function (App, View) {
+ App.UI.showModalDialog(new View({model: model}));
+ });
+ }
+ }
+ },
+
+ /**
+ * Logout
+ */
+ logout: function () {
+ Tokens.dropTopToken();
+ this.showLogin();
+ }
};
diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js
index c2e82f855..ba4a99a67 100644
--- a/frontend/js/app/dashboard/main.js
+++ b/frontend/js/app/dashboard/main.js
@@ -6,87 +6,85 @@ const Helpers = require('../../lib/helpers');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
- template: template,
- id: 'dashboard',
- columns: 0,
-
- stats: {},
-
- ui: {
- links: 'a'
- },
-
- events: {
- 'click @ui.links': function (e) {
- e.preventDefault();
- Controller.navigate($(e.currentTarget).attr('href'), true);
- }
- },
-
- templateContext: function () {
- let view = this;
-
- return {
- getUserName: function () {
- return Cache.User.get('nickname') || Cache.User.get('name');
- },
-
- getHostStat: function (type) {
- if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
- return Helpers.niceNumber(view.stats.hosts[type]);
- }
-
- return '-';
- },
-
- canShow: function (perm) {
- return Cache.User.isAdmin() || Cache.User.canView(perm);
- },
-
- columns: view.columns
- };
- },
-
- onRender: function () {
- let view = this;
-
- if (typeof view.stats.hosts === 'undefined') {
- Api.Reports.getHostStats()
- .then(response => {
- if (!view.isDestroyed()) {
- view.stats.hosts = response;
- view.render();
- }
- })
- .catch(err => {
- console.log(err);
- });
- }
- },
-
- /**
- * @param {Object} [model]
- */
- preRender: function (model) {
- this.columns = 0;
-
- // calculate the available columns based on permissions for the objects
- // and store as a variable
- //let view = this;
- let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
-
- perms.map(perm => {
- this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
- });
-
- // Prevent double rendering on initial calls
- if (typeof model !== 'undefined') {
- this.render();
- }
- },
-
- initialize: function () {
- this.preRender();
- this.listenTo(Cache.User, 'change', this.preRender);
- }
+ template: template,
+ id: 'dashboard',
+ columns: 0,
+
+ stats: {},
+
+ ui: {
+ links: 'a'
+ },
+
+ events: {
+ 'click @ui.links': function (e) {
+ e.preventDefault();
+ Controller.navigate($(e.currentTarget).attr('href'), true);
+ }
+ },
+
+ templateContext: function () {
+ const view = this;
+
+ return {
+ getUserName: function () {
+ return Cache.User.get('nickname') || Cache.User.get('name');
+ },
+
+ getHostStat: function (type) {
+ if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') {
+ return Helpers.niceNumber(view.stats.hosts[type]);
+ }
+
+ return '-';
+ },
+
+ canShow: function (perm) {
+ return Cache.User.isAdmin() || Cache.User.canView(perm);
+ },
+
+ columns: view.columns
+ };
+ },
+
+ onRender: function () {
+ const view = this;
+ if (typeof view.stats.hosts === 'undefined') {
+ Api.Reports.getHostStats()
+ .then(response => {
+ if (!view.isDestroyed()) {
+ view.stats.hosts = response;
+ view.render();
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ },
+
+ /**
+ * @param {Object} [model]
+ */
+ preRender: function (model) {
+ this.columns = 0;
+
+ // calculate the available columns based on permissions for the objects
+ // and store as a variable
+ const perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts'];
+
+ perms.map(perm => {
+ this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0;
+ });
+
+ // Prevent double rendering on initial calls
+ if (typeof model !== 'undefined') {
+ this.render();
+ }
+ },
+
+ initialize: function () {
+ this.preRender();
+ this.listenTo(Cache.User, 'change', this.preRender);
+ }
});
diff --git a/frontend/js/app/nginx/certificates/list/item.ejs b/frontend/js/app/nginx/certificates/list/item.ejs
index 9a0d6b27d..179a81955 100644
--- a/frontend/js/app/nginx/certificates/list/item.ejs
+++ b/frontend/js/app/nginx/certificates/list/item.ejs
@@ -33,6 +33,13 @@