diff --git a/.version b/.version
index d8b69897..3cf561c0 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-2.12.0
+2.12.1
diff --git a/Jenkinsfile b/Jenkinsfile
index 75757946..302e05b2 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -43,7 +43,7 @@ pipeline {
steps {
script {
// Defaults to the Branch name, which is applies to all branches AND pr's
- buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
+ buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
}
}
}
@@ -127,6 +127,11 @@ pipeline {
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
+ unstable {
+ dir(path: 'testing/results') {
+ archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
+ }
+ }
}
}
stage('Test Mysql') {
@@ -155,6 +160,11 @@ pipeline {
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
+ unstable {
+ dir(path: 'testing/results') {
+ archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
+ }
+ }
}
}
stage('MultiArch Build') {
@@ -193,7 +203,13 @@ pipeline {
}
steps {
script {
- npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true)
+ 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}`
+
+**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
+""", true)
}
}
}
diff --git a/README.md b/README.md
index 2d1b8da5..9ac6a6c8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
+
diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js
index 9bdfe695..34b8fdf5 100644
--- a/backend/internal/certificate.js
+++ b/backend/internal/certificate.js
@@ -3,27 +3,29 @@ const fs = require('fs');
const https = require('https');
const tempWrite = require('temp-write');
const moment = require('moment');
+const archiver = require('archiver');
+const path = require('path');
+const { isArray } = require('lodash');
const logger = require('../logger').ssl;
const config = require('../lib/config');
const error = require('../lib/error');
const utils = require('../lib/utils');
+const certbot = require('../lib/certbot');
const certificateModel = require('../models/certificate');
const tokenModel = require('../models/token');
const dnsPlugins = require('../global/certbot-dns-plugins.json');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const internalHost = require('./host');
-const certbot = require('../lib/certbot');
-const archiver = require('archiver');
-const path = require('path');
-const { isArray } = require('lodash');
+
const letsencryptStaging = config.useLetsencryptStaging();
+const letsencryptServer = config.useLetsencryptServer();
const letsencryptConfig = '/etc/letsencrypt.ini';
const certbotCommand = 'certbot';
function omissions() {
- return ['is_deleted'];
+ return ['is_deleted', 'owner.is_deleted'];
}
const internalCertificate = {
@@ -207,6 +209,7 @@ const internalCertificate = {
.patchAndFetchById(certificate.id, {
expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
})
+ .then(utils.omitRow(omissions()))
.then((saved_row) => {
// Add cert data for audit log
saved_row.meta = _.assign({}, saved_row.meta, {
@@ -730,29 +733,29 @@ const internalCertificate = {
return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
.then((result) => {
+ // Examples:
+ // subject=CN = *.jc21.com
// subject=CN = something.example.com
const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
const match = regex.exec(result);
-
- if (typeof match[1] === 'undefined') {
- throw new error.ValidationError('Could not determine subject from certificate: ' + result);
+ if (match && typeof match[1] !== 'undefined') {
+ certData['cn'] = match[1];
}
-
- certData['cn'] = match[1];
})
.then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
})
+
.then((result) => {
+ // Examples:
// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
+ // issuer=C = US, O = Let's Encrypt, CN = E5
+ // issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA
const regex = /^(?:issuer=)?(.*)$/gim;
const match = regex.exec(result);
-
- if (typeof match[1] === 'undefined') {
- throw new error.ValidationError('Could not determine issuer from certificate: ' + result);
+ if (match && typeof match[1] !== 'undefined') {
+ certData['issuer'] = match[1];
}
-
- certData['issuer'] = match[1];
})
.then(() => {
return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
@@ -827,17 +830,18 @@ const internalCertificate = {
requestLetsEncryptSsl: (certificate) => {
logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
- const cmd = certbotCommand + ' certonly ' +
- '--config "' + letsencryptConfig + '" ' +
+ const cmd = `${certbotCommand} certonly ` +
+ `--config '${letsencryptConfig}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
- '--cert-name "npm-' + certificate.id + '" ' +
+ `--cert-name "npm-${certificate.id}" ` +
'--agree-tos ' +
'--authenticator webroot ' +
- '--email "' + certificate.meta.letsencrypt_email + '" ' +
+ `--email '${certificate.meta.letsencrypt_email}' ` +
'--preferred-challenges "dns,http" ' +
- '--domains "' + certificate.domain_names.join(',') + '" ' +
- (letsencryptStaging ? '--staging' : '');
+ `--domains "${certificate.domain_names.join(',')}" ` +
+ (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+ (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
logger.info('Command:', cmd);
@@ -868,25 +872,26 @@ const internalCertificate = {
const hasConfigArg = certificate.meta.dns_provider !== 'route53';
let mainCmd = certbotCommand + ' certonly ' +
- '--config "' + letsencryptConfig + '" ' +
+ `--config '${letsencryptConfig}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
- '--cert-name "npm-' + certificate.id + '" ' +
+ `--cert-name 'npm-${certificate.id}' ` +
'--agree-tos ' +
- '--email "' + certificate.meta.letsencrypt_email + '" ' +
- '--domains "' + certificate.domain_names.join(',') + '" ' +
- '--authenticator ' + dnsPlugin.full_plugin_name + ' ' +
+ `--email '${certificate.meta.letsencrypt_email}' ` +
+ `--domains '${certificate.domain_names.join(',')}' ` +
+ `--authenticator '${dnsPlugin.full_plugin_name}' ` +
(
hasConfigArg
- ? '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
+ ? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' `
: ''
) +
(
certificate.meta.propagation_seconds !== undefined
- ? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
+ ? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' `
: ''
) +
- (letsencryptStaging ? ' --staging' : '');
+ (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+ (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
@@ -963,14 +968,15 @@ const internalCertificate = {
logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const cmd = certbotCommand + ' renew --force-renewal ' +
- '--config "' + letsencryptConfig + '" ' +
+ `--config '${letsencryptConfig}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
- '--cert-name "npm-' + certificate.id + '" ' +
+ `--cert-name 'npm-${certificate.id}' ` +
'--preferred-challenges "dns,http" ' +
'--no-random-sleep-on-renew ' +
'--disable-hook-validation ' +
- (letsencryptStaging ? '--staging' : '');
+ (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+ (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
logger.info('Command:', cmd);
@@ -995,13 +1001,14 @@ const internalCertificate = {
logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
let mainCmd = certbotCommand + ' renew --force-renewal ' +
- '--config "' + letsencryptConfig + '" ' +
+ `--config "${letsencryptConfig}" ` +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
- '--cert-name "npm-' + certificate.id + '" ' +
+ `--cert-name 'npm-${certificate.id}' ` +
'--disable-hook-validation ' +
'--no-random-sleep-on-renew ' +
- (letsencryptStaging ? ' --staging' : '');
+ (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+ (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Prepend the path to the credentials file as an environment variable
if (certificate.meta.dns_provider === 'route53') {
@@ -1027,12 +1034,13 @@ const internalCertificate = {
logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
const mainCmd = certbotCommand + ' revoke ' +
- '--config "' + letsencryptConfig + '" ' +
+ `--config '${letsencryptConfig}' ` +
'--work-dir "/tmp/letsencrypt-lib" ' +
'--logs-dir "/tmp/letsencrypt-log" ' +
- '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' +
+ `--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` +
'--delete-after-revoke ' +
- (letsencryptStaging ? '--staging' : '');
+ (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') +
+ (letsencryptStaging && letsencryptServer === null ? '--staging ' : '');
// Don't fail command if file does not exist
const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js
index 77933e73..5f802c00 100644
--- a/backend/internal/nginx.js
+++ b/backend/internal/nginx.js
@@ -181,7 +181,9 @@ const internalNginx = {
* @param {Object} host
* @returns {Promise}
*/
- generateConfig: (host_type, host) => {
+ generateConfig: (host_type, host_row) => {
+ // Prevent modifying the original object:
+ let host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
if (config.debug()) {
diff --git a/backend/lib/config.js b/backend/lib/config.js
index 174727b3..05f3fea8 100644
--- a/backend/lib/config.js
+++ b/backend/lib/config.js
@@ -199,5 +199,15 @@ module.exports = {
*/
useLetsencryptStaging: function () {
return !!process.env.LE_STAGING;
+ },
+
+ /**
+ * @returns {string|null}
+ */
+ useLetsencryptServer: function () {
+ if (process.env.LE_SERVER) {
+ return process.env.LE_SERVER;
+ }
+ return null;
}
};
diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json
index 04cd8980..b75dcf61 100644
--- a/backend/schema/components/certificate-object.json
+++ b/backend/schema/components/certificate-object.json
@@ -24,22 +24,34 @@
"description": "Nice Name for the custom certificate"
},
"domain_names": {
- "$ref": "../common.json#/properties/domain_names"
+ "description": "Domain Names separated by a comma",
+ "type": "array",
+ "maxItems": 100,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
+ }
},
"expires_on": {
"description": "Date and time of expiration",
"readOnly": true,
"type": "string"
},
+ "owner": {
+ "$ref": "./user-object.json"
+ },
"meta": {
"type": "object",
"additionalProperties": false,
"properties": {
- "letsencrypt_email": {
- "type": "string"
+ "certificate": {
+ "type": "string",
+ "minLength": 1
},
- "letsencrypt_agree": {
- "type": "boolean"
+ "certificate_key": {
+ "type": "string",
+ "minLength": 1
},
"dns_challenge": {
"type": "boolean"
@@ -50,13 +62,18 @@
"dns_provider_credentials": {
"type": "string"
},
+ "letsencrypt_agree": {
+ "type": "boolean"
+ },
+ "letsencrypt_certificate": {
+ "type": "object"
+ },
+ "letsencrypt_email": {
+ "type": "string"
+ },
"propagation_seconds": {
- "anyOf": [
- {
- "type": "integer",
- "minimum": 0
- }
- ]
+ "type": "integer",
+ "minimum": 0
}
}
}
diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json
index a64a58c8..5098802b 100644
--- a/backend/schema/components/proxy-host-object.json
+++ b/backend/schema/components/proxy-host-object.json
@@ -23,9 +23,7 @@
"locations",
"hsts_enabled",
"hsts_subdomains",
- "certificate",
- "use_default_location",
- "ipv6"
+ "certificate"
],
"additionalProperties": false,
"properties": {
@@ -151,12 +149,6 @@
"$ref": "./access-list-object.json"
}
]
- },
- "use_default_location": {
- "type": "boolean"
- },
- "ipv6": {
- "type": "boolean"
}
}
}
diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json
index cc4dbdd2..e7a495fd 100644
--- a/backend/schema/components/redirection-host-object.json
+++ b/backend/schema/components/redirection-host-object.json
@@ -28,7 +28,7 @@
},
"forward_scheme": {
"type": "string",
- "enum": ["http", "https"]
+ "enum": ["auto", "http", "https"]
},
"forward_domain_name": {
"description": "Domain Name",
diff --git a/backend/schema/components/setting-object.json b/backend/schema/components/setting-object.json
index e0877726..b9c6a103 100644
--- a/backend/schema/components/setting-object.json
+++ b/backend/schema/components/setting-object.json
@@ -25,7 +25,7 @@
"value": {
"description": "Value in almost any form",
"example": "congratulations",
- "oneOf": [
+ "anyOf": [
{
"type": "string",
"minLength": 1
@@ -46,7 +46,10 @@
},
"meta": {
"description": "Extra metadata",
- "example": {},
+ "example": {
+ "redirect": "http://example.com",
+ "html": "
404
"
+ },
"type": "object"
}
}
diff --git a/backend/schema/paths/nginx/certificates/certID/upload/post.json b/backend/schema/paths/nginx/certificates/certID/upload/post.json
index e9274856..f38b8102 100644
--- a/backend/schema/paths/nginx/certificates/certID/upload/post.json
+++ b/backend/schema/paths/nginx/certificates/certID/upload/post.json
@@ -55,6 +55,25 @@
"certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n"
}
}
+ },
+ "schema": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["certificate", "certificate_key"],
+ "properties": {
+ "certificate": {
+ "type": "string",
+ "minLength": 1
+ },
+ "certificate_key": {
+ "type": "string",
+ "minLength": 1
+ },
+ "intermediate_certificate": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
}
}
}
diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/put.json b/backend/schema/paths/nginx/dead-hosts/hostID/put.json
index 6a0a57e3..f9505ed4 100644
--- a/backend/schema/paths/nginx/dead-hosts/hostID/put.json
+++ b/backend/schema/paths/nginx/dead-hosts/hostID/put.json
@@ -94,9 +94,7 @@
"avatar": "",
"roles": ["admin"]
},
- "certificate": null,
- "use_default_location": true,
- "ipv6": true
+ "certificate": null
}
}
},
diff --git a/backend/schema/paths/nginx/dead-hosts/post.json b/backend/schema/paths/nginx/dead-hosts/post.json
index 59313506..c8bbb693 100644
--- a/backend/schema/paths/nginx/dead-hosts/post.json
+++ b/backend/schema/paths/nginx/dead-hosts/post.json
@@ -79,9 +79,7 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
- },
- "use_default_location": true,
- "ipv6": true
+ }
}
}
},
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
index af73905d..5cab6e75 100644
--- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
@@ -129,9 +129,7 @@
"roles": ["admin"]
},
"certificate": null,
- "access_list": null,
- "use_default_location": true,
- "ipv6": true
+ "access_list": null
}
}
},
diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json
index 13f64161..85455fb6 100644
--- a/backend/schema/paths/nginx/proxy-hosts/post.json
+++ b/backend/schema/paths/nginx/proxy-hosts/post.json
@@ -114,9 +114,7 @@
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": ["admin"]
},
- "access_list": null,
- "use_default_location": true,
- "ipv6": true
+ "access_list": null
}
}
},
diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json
index 870f16fc..fd97cbfa 100644
--- a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json
+++ b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json
@@ -114,9 +114,7 @@
"avatar": "",
"roles": ["admin"]
},
- "certificate": null,
- "use_default_location": true,
- "ipv6": true
+ "certificate": null
}
}
},
diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json
index 3a9a05fe..5bfde2c3 100644
--- a/backend/schema/paths/nginx/redirection-hosts/post.json
+++ b/backend/schema/paths/nginx/redirection-hosts/post.json
@@ -99,9 +99,7 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
- },
- "use_default_location": true,
- "ipv6": true
+ }
}
}
},
diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json
index f3ef54d4..fbfdc901 100644
--- a/backend/schema/paths/nginx/streams/streamID/put.json
+++ b/backend/schema/paths/nginx/streams/streamID/put.json
@@ -129,9 +129,7 @@
"roles": ["admin"]
},
"certificate": null,
- "access_list": null,
- "use_default_location": true,
- "ipv6": true
+ "access_list": null
}
}
},
diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json
index 5888ec05..4ca62429 100644
--- a/backend/schema/paths/settings/settingID/put.json
+++ b/backend/schema/paths/settings/settingID/put.json
@@ -13,7 +13,8 @@
"name": "settingID",
"schema": {
"type": "string",
- "minLength": 1
+ "minLength": 1,
+ "enum": ["default-site"]
},
"required": true,
"description": "Setting ID",
@@ -31,10 +32,21 @@
"minProperties": 1,
"properties": {
"value": {
- "$ref": "../../../components/setting-object.json#/properties/value"
+ "type": "string",
+ "minLength": 1,
+ "enum": ["congratulations", "404", "444", "redirect", "html"]
},
"meta": {
- "$ref": "../../../components/setting-object.json#/properties/meta"
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "redirect": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ }
+ }
}
}
}
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 799ee2a6..0603e2de 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -3,6 +3,8 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build
+FROM nginxproxymanager/testca AS testca
+FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM
@@ -45,6 +47,8 @@ RUN yarn install \
# add late to limit cache-busting by modifications
COPY docker/rootfs /
+COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
+COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
# Remove frontend service not required for prod, dev nginx config as well
RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index da2f8f06..7ce93c70 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -1,7 +1,10 @@
+FROM nginxproxymanager/testca AS testca
+FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
LABEL maintainer="Jamie Curnow "
-# See: https://github.com/just-containers/s6-overlay/blob/master/README.md
+SHELL ["/bin/bash", "-o", "pipefail", "-c"]
+
ENV SUPPRESS_NO_CONFIG_WARNING=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
@@ -17,18 +20,22 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& rm -rf /var/lib/apt/lists/*
# Task
-RUN cd /usr \
- && curl -sL https://taskfile.dev/install.sh | sh \
- && cd /root
+WORKDIR /usr
+RUN curl -sL https://taskfile.dev/install.sh | sh
+WORKDIR /root
COPY rootfs /
-RUN rm -f /etc/nginx/conf.d/production.conf
-RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager
-
-# s6 overlay
COPY scripts/install-s6 /tmp/install-s6
RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6
RUN chmod 644 -R /root/.cache
+RUN rm -f /etc/nginx/conf.d/production.conf \
+ && chmod 644 /etc/logrotate.d/nginx-proxy-manager \
+ && /tmp/install-s6 "${TARGETPLATFORM}" \
+ && rm -f /tmp/install-s6
+
+# Certs for testing purposes
+COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
+COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443
ENTRYPOINT [ "/init" ]
diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml
index e13e8bf2..bb68858f 100644
--- a/docker/docker-compose.ci.yml
+++ b/docker/docker-compose.ci.yml
@@ -9,6 +9,9 @@ services:
environment:
DEBUG: 'true'
FORCE_COLOR: 1
+ # Required for DNS Certificate provisioning in CI
+ LE_SERVER: 'https://ca.internal/acme/acme/directory'
+ REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
volumes:
- 'npm_data_ci:/data'
- 'npm_le_ci:/etc/letsencrypt'
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 4cdb1e91..2bfa2b79 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -1,7 +1,7 @@
# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production.
services:
- npm:
+ fullstack:
image: nginxproxymanager:dev
container_name: npm_core
build:
@@ -33,12 +33,20 @@ services:
DB_MYSQL_NAME: 'npm'
# DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true"
+ # Required for DNS Certificate provisioning testing:
+ LE_SERVER: 'https://ca.internal/acme/acme/directory'
+ REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
volumes:
- npm_data:/data
- le_data:/etc/letsencrypt
+ - './dev/resolv.conf:/etc/resolv.conf:ro'
- ../backend:/app
- ../frontend:/app/frontend
- ../global:/app/global
+ healthcheck:
+ test: ["CMD", "/usr/bin/check-health"]
+ interval: 10s
+ timeout: 3s
depends_on:
- db
working_dir: /app
@@ -58,6 +66,23 @@ services:
volumes:
- db_data:/var/lib/mysql
+ stepca:
+ image: jc21/testca
+ volumes:
+ - './dev/resolv.conf:/etc/resolv.conf:ro'
+ - '/etc/localtime:/etc/localtime:ro'
+ networks:
+ nginx_proxy_manager:
+ aliases:
+ - ca.internal
+
+ dnsrouter:
+ image: jc21/dnsrouter
+ volumes:
+ - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
+ networks:
+ - nginx_proxy_manager
+
swagger:
image: swaggerapi/swagger-ui:latest
container_name: npm_swagger
@@ -67,19 +92,78 @@ services:
URL: "http://npm:81/api/schema"
PORT: '80'
depends_on:
- - npm
+ - fullstack
squid:
image: ubuntu/squid
container_name: npm_squid
volumes:
- './dev/squid.conf:/etc/squid/squid.conf:ro'
+ - './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro'
networks:
- nginx_proxy_manager
ports:
- 8128:3128
+ pdns:
+ image: pschiffe/pdns-mysql
+ volumes:
+ - '/etc/localtime:/etc/localtime:ro'
+ environment:
+ PDNS_master: 'yes'
+ PDNS_api: 'yes'
+ PDNS_api_key: 'npm'
+ PDNS_webserver: 'yes'
+ PDNS_webserver_address: '0.0.0.0'
+ PDNS_webserver_password: 'npm'
+ PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
+ PDNS_version_string: 'anonymous'
+ PDNS_default_ttl: 1500
+ PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
+ PDNS_gmysql_host: pdns-db
+ PDNS_gmysql_port: 3306
+ PDNS_gmysql_user: pdns
+ PDNS_gmysql_password: pdns
+ PDNS_gmysql_dbname: pdns
+ depends_on:
+ - pdns-db
+ networks:
+ nginx_proxy_manager:
+ aliases:
+ - ns1.pdns
+ - ns2.pdns
+
+ pdns-db:
+ image: mariadb
+ environment:
+ MYSQL_ROOT_PASSWORD: 'pdns'
+ MYSQL_DATABASE: 'pdns'
+ MYSQL_USER: 'pdns'
+ MYSQL_PASSWORD: 'pdns'
+ volumes:
+ - 'pdns_mysql:/var/lib/mysql'
+ - '/etc/localtime:/etc/localtime:ro'
+ - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
+ networks:
+ - nginx_proxy_manager
+
+ cypress:
+ image: "npm_dev_cypress"
+ build:
+ context: ../
+ dockerfile: test/cypress/Dockerfile
+ environment:
+ HTTP_PROXY: 'squid:3128'
+ HTTPS_PROXY: 'squid:3128'
+ volumes:
+ - '../test/results:/results'
+ - './dev/resolv.conf:/etc/resolv.conf:ro'
+ - '/etc/localtime:/etc/localtime:ro'
+ command: cypress run --browser chrome --config-file=cypress/config/ci.js
+ networks:
+ - nginx_proxy_manager
+
volumes:
npm_data:
name: npm_core_data
@@ -87,6 +171,8 @@ volumes:
name: npm_le_data
db_data:
name: npm_db_data
+ pdns_mysql:
+ name: npm_pdns_mysql
networks:
nginx_proxy_manager:
diff --git a/scripts/.common.sh b/scripts/.common.sh
index 3cea0916..4111db3b 100644
--- a/scripts/.common.sh
+++ b/scripts/.common.sh
@@ -15,3 +15,13 @@ COMPOSE_PROJECT_NAME="npmdev"
COMPOSE_FILE="docker/docker-compose.dev.yml"
export COMPOSE_FILE COMPOSE_PROJECT_NAME
+
+# $1: container_name
+get_container_ip () {
+ local container_name=$1
+ local container
+ local ip
+ container=$(docker-compose ps --all -q "${container_name}" | tail -n1)
+ ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
+ echo "$ip"
+}
diff --git a/scripts/cypress-dev b/scripts/cypress-dev
new file mode 100755
index 00000000..a0c64ad9
--- /dev/null
+++ b/scripts/cypress-dev
@@ -0,0 +1,13 @@
+#!/bin/bash -e
+
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+. "$DIR/.common.sh"
+
+# Ensure docker-compose exists
+if hash docker-compose 2>/dev/null; then
+ cd "${DIR}/.."
+ rm -rf "$DIR/../test/results"
+ docker-compose up --build cypress
+else
+ echo -e "${RED}❯ docker-compose command is not available${RESET}"
+fi
diff --git a/scripts/start-dev b/scripts/start-dev
index f064a4bd..9da6ed45 100755
--- a/scripts/start-dev
+++ b/scripts/start-dev
@@ -7,8 +7,43 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if hash docker-compose 2>/dev/null; then
cd "${DIR}/.."
echo -e "${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}"
+ echo -e "${BLUE}❯ $(docker-compose config)${RESET}"
- docker-compose up -d --remove-orphans --force-recreate --build
+ # Bring up a stack, in steps so we can inject IPs everywhere
+ docker-compose up -d pdns pdns-db
+ PDNS_IP=$(get_container_ip "pdns")
+ echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}"
+
+ # adjust the dnsrouter config
+ LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json"
+ rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp"
+ # IMPORTANT: changes to dnsrouter-config.json will affect this line:
+ jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp"
+
+ docker-compose up -d dnsrouter
+ DNSROUTER_IP=$(get_container_ip "dnsrouter")
+ echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}"
+
+ if [ "${DNSROUTER_IP:-}" = "" ]; then
+ echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}"
+ exit 1
+ fi
+
+ # mount the resolver
+ LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf"
+ rm -rf "${LOCAL_RESOLVE}"
+ printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
+
+ # bring up all remaining containers, except cypress!
+ docker-compose up -d --remove-orphans stepca squid
+ docker-compose pull db
+ docker-compose up -d --remove-orphans --pull=never fullstack
+ docker-compose up -d --remove-orphans swagger
+
+ # docker-compose up -d --remove-orphans --force-recreate --build
+
+ # wait for main container to be healthy
+ bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120
echo ""
echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}"
diff --git a/test/cypress/config/ci.js b/test/cypress/config/ci.js
index 873330dc..dc968dbd 100644
--- a/test/cypress/config/ci.js
+++ b/test/cypress/config/ci.js
@@ -15,7 +15,7 @@ module.exports = defineConfig({
return require("../plugins/index.js")(on, config);
},
env: {
- swaggerBase: 'http://fullstack:81/api/schema',
+ swaggerBase: '{{baseUrl}}/api/schema?ts=' + Date.now(),
},
baseUrl: 'http://fullstack:81',
}
diff --git a/test/cypress/config/dev.js b/test/cypress/config/dev.js
deleted file mode 100644
index f3c9f6d2..00000000
--- a/test/cypress/config/dev.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const { defineConfig } = require('cypress');
-
-module.exports = defineConfig({
- requestTimeout: 30000,
- defaultCommandTimeout: 20000,
- reporter: 'cypress-multi-reporters',
- reporterOptions: {
- configFile: 'multi-reporter.json'
- },
- video: false,
- videosFolder: 'results/videos',
- screenshotsFolder: 'results/screenshots',
- e2e: {
- setupNodeEvents(on, config) {
- return require("../plugins/index.js")(on, config);
- },
- env: {
- swaggerBase: 'http://npm:81/api/schema',
- },
- baseUrl: 'http://npm:81',
- }
-});
diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js
index 043680f3..1e8a6fed 100644
--- a/test/cypress/e2e/api/Certificates.cy.js
+++ b/test/cypress/e2e/api/Certificates.cy.js
@@ -1,7 +1,8 @@
-///
+///
describe('Certificates endpoints', () => {
let token;
+ let certID;
before(() => {
cy.getToken().then((tok) => {
@@ -24,6 +25,54 @@ describe('Certificates endpoints', () => {
});
});
+ it('Custom certificate lifecycle', function() {
+ // Create custom cert
+ cy.task('backendApiPost', {
+ token: token,
+ path: '/api/nginx/certificates',
+ data: {
+ provider: "other",
+ nice_name: "Test Certificate",
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+ expect(data).to.have.property('id');
+ certID = data.id;
+
+ // Upload files
+ cy.task('backendApiPostFiles', {
+ token: token,
+ path: `/api/nginx/certificates/${certID}/upload`,
+ files: {
+ certificate: 'test.example.com.pem',
+ certificate_key: 'test.example.com-key.pem',
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
+ expect(data).to.have.property('certificate');
+ expect(data).to.have.property('certificate_key');
+
+ // Get all certs
+ cy.task('backendApiGet', {
+ token: token,
+ path: '/api/nginx/certificates?expand=owner'
+ }).then((data) => {
+ cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);
+ expect(data.length).to.be.greaterThan(0);
+
+ // Delete cert
+ cy.task('backendApiDelete', {
+ token: token,
+ path: `/api/nginx/certificates/${certID}`
+ }).then((data) => {
+ cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);
+ expect(data).to.be.equal(true);
+ });
+ });
+ });
+ });
+ });
+
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() {
cy.task('backendApiPost', {
token: token,
diff --git a/test/cypress/e2e/api/FullCertProvision.cy.js b/test/cypress/e2e/api/FullCertProvision.cy.js
new file mode 100644
index 00000000..5ca5692c
--- /dev/null
+++ b/test/cypress/e2e/api/FullCertProvision.cy.js
@@ -0,0 +1,62 @@
+///
+
+describe('Full Certificate Provisions', () => {
+ let token;
+
+ before(() => {
+ cy.getToken().then((tok) => {
+ token = tok;
+ });
+ });
+
+ it('Should be able to create new http certificate', function() {
+ cy.task('backendApiPost', {
+ token: token,
+ path: '/api/nginx/certificates',
+ data: {
+ domain_names: [
+ 'website1.example.com'
+ ],
+ meta: {
+ letsencrypt_email: 'admin@example.com',
+ letsencrypt_agree: true,
+ dns_challenge: false
+ },
+ provider: 'letsencrypt'
+ }
+ }).then((data) => {
+ cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.greaterThan(0);
+ expect(data.provider).to.be.equal('letsencrypt');
+ });
+ });
+
+ it('Should be able to create new DNS certificate with Powerdns', function() {
+ cy.task('backendApiPost', {
+ token: token,
+ path: '/api/nginx/certificates',
+ data: {
+ domain_names: [
+ 'website2.example.com'
+ ],
+ meta: {
+ letsencrypt_email: "admin@example.com",
+ dns_challenge: true,
+ dns_provider: 'powerdns',
+ dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm',
+ letsencrypt_agree: true,
+ propagation_seconds: 5,
+ },
+ provider: 'letsencrypt'
+ }
+ }).then((data) => {
+ cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.greaterThan(0);
+ expect(data.provider).to.be.equal('letsencrypt');
+ expect(data.meta.dns_provider).to.be.equal('powerdns');
+ });
+ });
+
+});
diff --git a/test/cypress/e2e/api/Health.cy.js b/test/cypress/e2e/api/Health.cy.js
index f765c99b..49881e97 100644
--- a/test/cypress/e2e/api/Health.cy.js
+++ b/test/cypress/e2e/api/Health.cy.js
@@ -1,4 +1,4 @@
-///
+///
describe('Basic API checks', () => {
it('Should return a valid health payload', function () {
@@ -12,7 +12,7 @@ describe('Basic API checks', () => {
it('Should return a valid schema payload', function () {
cy.task('backendApiGet', {
- path: '/api/schema',
+ path: '/api/schema?ts=' + Date.now(),
}).then((data) => {
expect(data.openapi).to.be.equal('3.1.0');
});
diff --git a/test/cypress/e2e/api/Hosts.cy.js b/test/cypress/e2e/api/ProxyHosts.cy.js
similarity index 94%
rename from test/cypress/e2e/api/Hosts.cy.js
rename to test/cypress/e2e/api/ProxyHosts.cy.js
index 62b9581b..5bc64580 100644
--- a/test/cypress/e2e/api/Hosts.cy.js
+++ b/test/cypress/e2e/api/ProxyHosts.cy.js
@@ -1,6 +1,6 @@
-///
+///
-describe('Hosts endpoints', () => {
+describe('Proxy Hosts endpoints', () => {
let token;
before(() => {
diff --git a/test/cypress/e2e/api/Settings.cy.js b/test/cypress/e2e/api/Settings.cy.js
new file mode 100644
index 00000000..6942760c
--- /dev/null
+++ b/test/cypress/e2e/api/Settings.cy.js
@@ -0,0 +1,124 @@
+///
+
+describe('Settings endpoints', () => {
+ let token;
+
+ before(() => {
+ cy.getToken().then((tok) => {
+ token = tok;
+ });
+ });
+
+ it('Get all settings', function() {
+ cy.task('backendApiGet', {
+ token: token,
+ path: '/api/settings',
+ }).then((data) => {
+ cy.validateSwaggerSchema('get', 200, '/settings', data);
+ expect(data.length).to.be.greaterThan(0);
+ });
+ });
+
+ it('Get default-site setting', function() {
+ cy.task('backendApiGet', {
+ token: token,
+ path: '/api/settings/default-site',
+ }).then((data) => {
+ cy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ });
+ });
+
+ it('Default Site congratulations', function() {
+ cy.task('backendApiPut', {
+ token: token,
+ path: '/api/settings/default-site',
+ data: {
+ value: 'congratulations',
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ expect(data).to.have.property('value');
+ expect(data.value).to.be.equal('congratulations');
+ });
+ });
+
+ it('Default Site 404', function() {
+ cy.task('backendApiPut', {
+ token: token,
+ path: '/api/settings/default-site',
+ data: {
+ value: '404',
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ expect(data).to.have.property('value');
+ expect(data.value).to.be.equal('404');
+ });
+ });
+
+ it('Default Site 444', function() {
+ cy.task('backendApiPut', {
+ token: token,
+ path: '/api/settings/default-site',
+ data: {
+ value: '444',
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ expect(data).to.have.property('value');
+ expect(data.value).to.be.equal('444');
+ });
+ });
+
+ it('Default Site redirect', function() {
+ cy.task('backendApiPut', {
+ token: token,
+ path: '/api/settings/default-site',
+ data: {
+ value: 'redirect',
+ meta: {
+ redirect: 'https://www.google.com',
+ },
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ expect(data).to.have.property('value');
+ expect(data.value).to.be.equal('redirect');
+ expect(data).to.have.property('meta');
+ expect(data.meta).to.have.property('redirect');
+ expect(data.meta.redirect).to.be.equal('https://www.google.com');
+ });
+ });
+
+ it('Default Site html', function() {
+ cy.task('backendApiPut', {
+ token: token,
+ path: '/api/settings/default-site',
+ data: {
+ value: 'html',
+ meta: {
+ html: 'hello world
'
+ },
+ },
+ }).then((data) => {
+ cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data);
+ expect(data).to.have.property('id');
+ expect(data.id).to.be.equal('default-site');
+ expect(data).to.have.property('value');
+ expect(data.value).to.be.equal('html');
+ expect(data).to.have.property('meta');
+ expect(data.meta).to.have.property('html');
+ expect(data.meta.html).to.be.equal('hello world
');
+ });
+ });
+});
diff --git a/test/cypress/e2e/api/Users.cy.js b/test/cypress/e2e/api/Users.cy.js
index 43303d43..06b18317 100644
--- a/test/cypress/e2e/api/Users.cy.js
+++ b/test/cypress/e2e/api/Users.cy.js
@@ -1,4 +1,4 @@
-///
+///
describe('Users endpoints', () => {
let token;
diff --git a/test/cypress/plugins/backendApi/client.js b/test/cypress/plugins/backendApi/client.js
index e7c0c439..6f5f7661 100644
--- a/test/cypress/plugins/backendApi/client.js
+++ b/test/cypress/plugins/backendApi/client.js
@@ -7,7 +7,7 @@ const BackendApi = function(config, token) {
this.axios = axios.create({
baseURL: config.baseUrl,
- timeout: 5000,
+ timeout: 90000,
});
};
@@ -80,7 +80,7 @@ BackendApi.prototype._handleError = function(err, resolve, reject, returnOnError
* @returns {Promise