Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e6a83df84 | |||
| ec4910a45e | |||
| 6558c78094 | |||
| 5df92d1903 | |||
| 05affa7d26 | |||
| 46c6c5a5a8 | |||
| 75da751c72 | |||
| b84f60671e | |||
| 8dcb06cb02 | |||
| 83bf739081 | |||
| 48a52fae2e | |||
| 0ddbda6068 | |||
| 360fa058ea | |||
| 489d2022e6 | |||
| f762d0c0a1 | |||
| 98cad0678d | |||
| 92acb2954f | |||
| 00a6e4c982 | |||
| bf9eb4bd87 | |||
| 2f4940acbd | |||
| 9f7ca552a6 | |||
| 4272d5be8a | |||
| 1babfb6e87 | |||
| 5663cf45f8 | |||
| d8cb2d1d25 | |||
| 174a60bb07 | |||
| 3d7094bf28 | |||
| 4d6616930a | |||
| 24875ba292 | |||
| c58b2677b6 | |||
| 25146e1134 | |||
| c0c35964fe | |||
| 0bf9ab0a2b | |||
| 6d86f4cbda | |||
| d2741bbeb9 | |||
| 690d02a353 | |||
| c629db9597 | |||
| 67fcf85abb | |||
| 527eace8f8 | |||
| e65230b833 | |||
| 3e8334040b | |||
| 2bcd3a8e4d | |||
| e75b85fc3a | |||
| c4362d3339 | |||
| 85e492a632 | |||
| b8d4b67043 | |||
| ffacd31259 | |||
| 19f6da88da | |||
| c0faae4e27 | |||
| a19c566eea | |||
| 3ec806452c | |||
| 0c73cd5219 | |||
| 9b6bf719ff | |||
| 25431d3cc4 | |||
| e0805df3b1 | |||
| 8392fec570 | |||
| 1c173ca83f | |||
| 05a67db761 | |||
| bb24d5cf9e | |||
| 8d2fbe931f | |||
| 0a8adaac9f | |||
| fa6d151325 | |||
| a7296a0339 | |||
| a6aee53ec2 | |||
| 963ab2e791 | |||
| ca724b8b03 | |||
| 88a929c85e | |||
| 2bc0270880 | |||
| 014b77b7aa | |||
| 06f8aa8f29 | |||
| a8c64bf9f7 | |||
| 41ef16fbec | |||
| 2a848a481b | |||
| 3963d76a80 | |||
| 8ede37a43d | |||
| 36534f6bb2 | |||
| 7eddcaf708 | |||
| d8d2572aa1 | |||
| 96a98a74ac | |||
| d0a244e392 | |||
| f09c89e33f | |||
| d53f0679e5 | |||
| 527093ebcb | |||
| bd5835b866 | |||
| 6dd70c0ef2 | |||
| acc90e16d7 | |||
| 4b3aca7413 | |||
| 8daee764d2 | |||
| 3dedda32d4 | |||
| d127b25f0f | |||
| 6a2b0eedb3 | |||
| 8c81a97a4b | |||
| d9ab1a78d5 | |||
| 593df8ed49 | |||
| b30def3620 | |||
| 9c02785d49 | |||
| f747343159 | |||
| 2971910ccf | |||
| 56534b9647 | |||
| a8d26067ee | |||
| 4212e4bb00 | |||
| 7b27ace7bf | |||
| d8944da68d | |||
| 433d797cb7 | |||
| 0b1d940128 | |||
| 6016024026 | |||
| e199293229 | |||
| 2ebe92fec3 | |||
| 628cf1e3de | |||
| 9e9aaf68f0 | |||
| b595ca422c | |||
| 9273a6c726 | |||
| 76d00d4e65 | |||
| 668c03a11b | |||
| 1e72d2d651 | |||
| 89fc8efc67 |
@@ -1064,3 +1064,60 @@
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
* Fix issue where OAuth SSO did not work when alternate domain was used
|
||||
|
||||
[1.7.4]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add DigitalOcean spaces backup storage backend
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
* Fix issue where OAuth SSO did not work when alternate domain was used
|
||||
* Changelog is now rendered in markdown format
|
||||
|
||||
[1.7.5]
|
||||
* Expose a TLS relay port from mail container for Go applications
|
||||
|
||||
[1.7.6]
|
||||
* Port bindings cannot be configured in update route anymore
|
||||
* Implement LDAP group compare
|
||||
* Pre-releases are now offered by appstore and not handled in box code anymore
|
||||
* LDAP pagination support. This will fix the warnings in NextCloud and Rocket.Chat
|
||||
* Check if directories can be created in the backup directory
|
||||
* Do not set the HTTPS agent when using HTTP with minio backup backend
|
||||
* Fix regression where a new domain config could not be set in the UI
|
||||
* New mail container release that fixes email sending with SOGo
|
||||
* Show 404 page for unknown domains
|
||||
|
||||
[1.7.7]
|
||||
* Allow setting app memory till memory limit
|
||||
* Make the dkim selector dynamic
|
||||
* Fix issue where app update dialog did not close
|
||||
* Fix LE cert renewal failures
|
||||
* Send user and cert info in digest emails
|
||||
* Send oom, app failures and other important mails to cloudron owner's alt mail
|
||||
|
||||
[1.8.0]
|
||||
* Fix group email bounce when a group has users that have not signed up yet
|
||||
* Do not restrict app memory limit to 4GB
|
||||
* Fix display of the latest backup in the weekly digest
|
||||
* Add UI to select users for access restriction
|
||||
* Update docker to 17.09
|
||||
* Update node to 6.11.5
|
||||
* Display package version of installed apps in the info dialog
|
||||
|
||||
[1.8.1]
|
||||
* Update node modules
|
||||
* Allow a restore operation if app is already restoring
|
||||
* Remove pre-install bundle support since it was hardly used
|
||||
* Make the test email mail address configurable
|
||||
* Allow admins to access all apps
|
||||
* Send feedback via appstore API (instead of email)
|
||||
* Show documentation URL in the app info dialog
|
||||
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016 Cloudron UG
|
||||
Copyright (C) 2016,2017 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -47,10 +47,10 @@ apt-get -y install \
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-6.11.3
|
||||
curl -sL https://nodejs.org/dist/v6.11.3/node-v6.11.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.3
|
||||
ln -sf /usr/local/node-6.11.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.3/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
@@ -61,7 +61,7 @@ echo "==> Installing Docker"
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.03.1~ce-0~ubuntu-xenial_amd64.deb -o /tmp/docker.deb
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
|
||||
+3
-2
@@ -6,9 +6,9 @@ var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
del = require('del'),
|
||||
ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
@@ -196,7 +196,8 @@ gulp.task('watch', ['default'], function () {
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
del.sync(['webadmin/dist', 'setup/splash/website']);
|
||||
rimraf.sync('webadmin/dist');
|
||||
rimraf.sync('setup/splash/website');
|
||||
});
|
||||
|
||||
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN manifestJson TEXT'),
|
||||
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
// fill all the backups with restoreConfigs from current apps
|
||||
function addManifests(callback) {
|
||||
console.log('Importing manifests');
|
||||
|
||||
db.all('SELECT * FROM backups WHERE type="app"', function (error, backups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(backups, function (backup, next) {
|
||||
var m = backup.restoreConfigJson ? JSON.parse(backup.restoreConfigJson) : null;
|
||||
if (m) m = JSON.stringify(m.manifest);
|
||||
|
||||
db.runSql('UPDATE backups SET manifestJson=? WHERE id=?', [ m, backup.id ], next);
|
||||
}, callback);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
|
||||
// remove the restoreConfig
|
||||
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN restoreConfigJson')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN manifestJson'),
|
||||
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN restoreConfigJson TEXT'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE newConfigJson updateConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE updateConfigJson newConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE lastBackupId restoreConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE restoreConfigJson lastBackupId TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -72,9 +72,9 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
enableBackup BOOLEAN DEFAULT 1,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
|
||||
newConfigJson TEXT, // used to pass new config for apptask (update)
|
||||
updateConfigJson TEXT, // used to pass new config for apptask (update)
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS backups(
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
state VARCHAR(16) NOT NULL,
|
||||
restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */
|
||||
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
||||
format VARCHAR(16) DEFAULT "tgz",
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
Generated
+976
-1016
File diff suppressed because it is too large
Load Diff
+39
-38
@@ -14,89 +14,90 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.6.2",
|
||||
"@google-cloud/dns": "^0.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.5.0",
|
||||
"aws-sdk": "^2.132.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"cloudron-manifestformat": "^2.9.0",
|
||||
"async": "^2.6.0",
|
||||
"aws-sdk": "^2.151.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cloudron-manifestformat": "^2.10.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^0.1.0",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.1.0",
|
||||
"cron": "^1.0.9",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.10.0-beta.20",
|
||||
"db-migrate": "^0.10.0-beta.24",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^3.0.0",
|
||||
"dockerode": "^2.4.3",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.3",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.15.4",
|
||||
"express-session": "^1.15.5",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^1.3.4",
|
||||
"moment-timezone": "^0.5.5",
|
||||
"morgan": "^1.7.0",
|
||||
"mime": "^2.0.3",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"nodemailer": "^4.0.1",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.4.0",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.0.1",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
"parse-links": "^0.1.0",
|
||||
"passport": "^0.2.2",
|
||||
"passport-http": "^0.2.2",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^2.0.2",
|
||||
"password-generator": "^2.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"request": "^2.83.0",
|
||||
"s3-block-read-stream": "^0.2.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^4.3.6",
|
||||
"showdown": "^1.6.0",
|
||||
"semver": "^5.4.1",
|
||||
"showdown": "^1.8.2",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.5.2",
|
||||
"superagent": "^3.8.1",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "^1.15.3",
|
||||
"tldjs": "^1.6.2",
|
||||
"tar-fs": "^1.16.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"tldjs": "^2.2.0",
|
||||
"underscore": "^1.7.0",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^4.9.0",
|
||||
"ws": "^2.3.1"
|
||||
"validator": "^9.1.1",
|
||||
"ws": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"del": "^1.1.1",
|
||||
"expect.js": "*",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^2.3.0",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^1.0.0",
|
||||
"gulp-ejs": "^3.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"hock": "~1.2.0",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "*",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^3.13.1",
|
||||
"node-sass": "^4.6.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"request": "^2.65.0",
|
||||
"yargs": "^3.15.0"
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
|
||||
+12
-19
@@ -45,6 +45,7 @@ fi
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
domain=""
|
||||
adminLocation="my"
|
||||
zoneName=""
|
||||
provider=""
|
||||
encryptionKey=""
|
||||
@@ -63,13 +64,14 @@ baseDataDir=""
|
||||
# TODO this is still there for the restore case, see other occasions below
|
||||
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/references/selfhosting.html on how to install Cloudron"; exit 0;;
|
||||
--admin-location) adminLocation="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
@@ -105,12 +107,12 @@ done
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "cloudscale" && \
|
||||
"${provider}" != "cloudscale.ch" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
@@ -123,7 +125,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -197,6 +199,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
@@ -214,9 +217,6 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"format": "tgz",
|
||||
"retentionSecs": 172800
|
||||
},
|
||||
"updateConfig": {
|
||||
"prerelease": ${prerelease}
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
@@ -226,6 +226,7 @@ EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
@@ -262,17 +263,9 @@ fi
|
||||
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
# poor mans semver
|
||||
if [[ ${version} == "0.10"* ]]; then
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
rm "${DATA_FILE}"
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v6.11.3" ]]; then
|
||||
echo "This script requires node 6.11.3"
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
echo "This script requires node 6.11.5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+34
-9
@@ -34,13 +34,41 @@ while true; do
|
||||
esac
|
||||
done
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
|
||||
echo "docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! dpkg --force-confold --configure -a; do
|
||||
echo "Failed to fix packages. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/docker.deb; do
|
||||
echo "Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm /tmp/docker.deb
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v6.11.3" ]]; then
|
||||
mkdir -p /usr/local/node-6.11.3
|
||||
$curl -sL https://nodejs.org/dist/v6.11.3/node-v6.11.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.3
|
||||
ln -sf /usr/local/node-6.11.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.3/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.2
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
$curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.3
|
||||
fi
|
||||
|
||||
for try in `seq 1 10`; do
|
||||
@@ -81,9 +109,6 @@ fi
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
echo "==> installer: updating packages"
|
||||
# add logic to update apt packages here
|
||||
|
||||
echo "==> installer: switching the box code"
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
|
||||
+5
-9
@@ -6,6 +6,7 @@ json="${source_dir}/../node_modules/.bin/json"
|
||||
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn=""
|
||||
arg_admin_location=""
|
||||
arg_zone_name=""
|
||||
arg_is_custom_domain="false"
|
||||
arg_restore_key=""
|
||||
@@ -20,9 +21,7 @@ arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_backup_config=""
|
||||
arg_dns_config=""
|
||||
arg_update_config=""
|
||||
arg_provider=""
|
||||
arg_app_bundle=""
|
||||
arg_is_demo="false"
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
@@ -46,19 +45,19 @@ while true; do
|
||||
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
|
||||
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
|
||||
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
|
||||
|
||||
# TODO check if an where this is used
|
||||
# TODO check if and where this is used
|
||||
arg_version=$(echo "$2" | $json version)
|
||||
|
||||
# read possibly empty parameters here
|
||||
arg_app_bundle=$(echo "$2" | $json appBundle)
|
||||
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
|
||||
|
||||
arg_is_demo=$(echo "$2" | $json isDemo)
|
||||
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
|
||||
|
||||
@@ -86,9 +85,6 @@ while true; do
|
||||
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||
|
||||
arg_update_config=$(echo "$2" | $json updateConfig)
|
||||
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
@@ -19,7 +18,7 @@ fi
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
# keep this is sync with config.js appFqdn()
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
|
||||
admin_origin="https://${admin_fqdn}"
|
||||
|
||||
# copy the website
|
||||
|
||||
+29
-37
@@ -95,11 +95,6 @@ mkdir -p "${BOX_DATA_DIR}/mail/dkim"
|
||||
mkdir -p /var/backups
|
||||
chmod 777 /var/backups
|
||||
|
||||
echo "==> Migrating mail data"
|
||||
if [[ -d "${PLATFORM_DATA_DIR}/mail" ]]; then
|
||||
find "${PLATFORM_DATA_DIR}/mail" -mindepth 1 -maxdepth 1 -exec mv --target-directory="${BOX_DATA_DIR}/mail" '{}' +
|
||||
fi
|
||||
|
||||
echo "==> Configuring journald"
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
@@ -191,7 +186,11 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
|
||||
echo "Waiting for mysql jobs..."
|
||||
sleep 1
|
||||
done
|
||||
systemctl restart mysql
|
||||
while true; do
|
||||
if systemctl restart mysql; then break; fi
|
||||
echo "Restarting MySql again after sometime since this fails randomly"
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
systemctl start mysql
|
||||
fi
|
||||
@@ -243,6 +242,24 @@ cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
mysql -u root -p${mysql_root_password} -e "REPLACE INTO settings (name, value) VALUES (\"domain\", '{ \"fqdn\": \"$arg_fqdn\", \"zoneName\": \"$arg_zone_name\", \"adminLocation\": \"$arg_admin_location\" }')" box
|
||||
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
@@ -251,18 +268,11 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"fqdn": "${arg_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"zoneName": "${arg_zone_name}",
|
||||
"isCustomDomain": ${arg_is_custom_domain},
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo},
|
||||
"database": {
|
||||
"hostname": "127.0.0.1",
|
||||
"username": "root",
|
||||
"password": "${mysql_root_password}",
|
||||
"port": 3306,
|
||||
"name": "box"
|
||||
},
|
||||
"appBundle": ${arg_app_bundle}
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
# pass these out-of-band because they have new lines which interfere with json
|
||||
@@ -289,31 +299,13 @@ fi
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_update_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
|
||||
set_progress "60" "Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
|
||||
@@ -4,6 +4,41 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# http server
|
||||
server {
|
||||
listen 80;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:80;
|
||||
<% } -%>
|
||||
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
<% } else { -%>
|
||||
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
|
||||
server_name "~^\d+\.\d+\.\d+\.\d+$";
|
||||
|
||||
# collectd
|
||||
location /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
<% } -%>
|
||||
|
||||
# acme challenges (for cert renewal where the vhost config exists)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# https server
|
||||
server {
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
|
||||
@@ -36,28 +36,21 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
# HTTP server
|
||||
|
||||
# default http server that returns 404 for any domain we are not listening on
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name does_not_match_anything;
|
||||
|
||||
# collectd
|
||||
location /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# acme challenges
|
||||
# acme challenges (for app installation and re-configure when the vhost config does not exist)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -363,6 +363,7 @@ function setupSendMail(app, options, callback) {
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
|
||||
|
||||
+27
-10
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
delPortBinding: delPortBinding,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
@@ -59,7 +60,7 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.newConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
@@ -75,9 +76,13 @@ function postProcess(result) {
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
|
||||
assert(result.newConfigJson === null || typeof result.newConfigJson === 'string');
|
||||
result.newConfig = safe.JSON.parse(result.newConfigJson);
|
||||
delete result.newConfigJson;
|
||||
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
|
||||
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
|
||||
delete result.updateConfigJson;
|
||||
|
||||
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
|
||||
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
|
||||
delete result.restoreConfigJson;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
@@ -192,14 +197,14 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
var altDomain = data.altDomain || null;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var lastBackupId = data.lastBackupId || null; // used when cloning
|
||||
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
|
||||
var queries = [];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -252,6 +257,18 @@ function getPortBindings(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function delPortBinding(hostPort, callback) {
|
||||
assert.strictEqual(typeof hostPort, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -309,7 +326,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'newConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings') {
|
||||
@@ -363,14 +380,14 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state
|
||||
// restore is allowed from installed or error state or currently restoring
|
||||
// configure is allowed in installed state or currently configuring or in error state
|
||||
// update and backup are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
|
||||
|
||||
+56
-48
@@ -65,6 +65,7 @@ var addons = require('./addons.js'),
|
||||
groups = require('./groups.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -118,7 +119,7 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name
|
||||
function validateHostname(location, fqdn) {
|
||||
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
|
||||
var RESERVED_LOCATIONS = [ config.adminLocation(), constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, config.mailLocation(), constants.POSTMAN_LOCATION ];
|
||||
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
@@ -208,7 +209,7 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
|
||||
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
var max = (4096 * 1024 * 1024);
|
||||
var max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
|
||||
|
||||
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
|
||||
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
|
||||
@@ -258,6 +259,12 @@ function validateRobotsTxt(robotsTxt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateBackupFormat(format) {
|
||||
if (format === 'tgz' || format == 'rsync') return null;
|
||||
|
||||
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -308,12 +315,18 @@ function hasAccessTo(app, user, callback) {
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
||||
|
||||
// check group access
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
groups.getGroups(user.id, function (error, groupIds) {
|
||||
if (error) return callback(null, false);
|
||||
|
||||
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
|
||||
groups.isMember(groupId, user.id, iteratorDone);
|
||||
}, function (error, result) {
|
||||
callback(null, !error && result);
|
||||
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
|
||||
|
||||
if (isAdmin) return callback(null, true); // admins can always access any app
|
||||
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
|
||||
callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,7 +433,8 @@ function install(data, auditSource, callback) {
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null;
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz';
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -454,6 +468,9 @@ function install(data, auditSource, callback) {
|
||||
error = validateRobotsTxt(robotsTxt);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateBackupFormat(backupFormat);
|
||||
if (error) return callback(error);
|
||||
|
||||
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
@@ -489,7 +506,7 @@ function install(data, auditSource, callback) {
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
lastBackupId: backupId,
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
};
|
||||
@@ -629,7 +646,7 @@ function update(appId, data, auditSource, callback) {
|
||||
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var newConfig = { };
|
||||
var updateConfig = { };
|
||||
|
||||
error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
|
||||
@@ -637,15 +654,7 @@ function update(appId, data, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
newConfig.manifest = manifest;
|
||||
|
||||
// TODO: disallow portBindings when an app updates and let ports simply be disabled. the new ports
|
||||
// might conflict when the update is actually carried out as we do not 'reserve' them in the db
|
||||
if ('portBindings' in data) {
|
||||
newConfig.portBindings = data.portBindings;
|
||||
error = validatePortBindings(data.portBindings, newConfig.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
updateConfig.manifest = manifest;
|
||||
|
||||
if ('icon' in data) {
|
||||
if (data.icon) {
|
||||
@@ -665,22 +674,22 @@ function update(appId, data, auditSource, callback) {
|
||||
|
||||
// prevent user from installing a app with different manifest id over an existing app
|
||||
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
||||
if (app.manifest.id !== newConfig.manifest.id) {
|
||||
if (app.manifest.id !== updateConfig.manifest.id) {
|
||||
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
|
||||
// clear appStoreId so that this app does not get updates anymore
|
||||
newConfig.appStoreId = '';
|
||||
updateConfig.appStoreId = '';
|
||||
}
|
||||
|
||||
// do not update apps in debug mode
|
||||
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
|
||||
|
||||
// Ensure we update the memory limit in case the new app requires more memory as a minimum
|
||||
// 0 and -1 are special newConfig for memory limit indicating unset and unlimited
|
||||
if (app.memoryLimit > 0 && newConfig.manifest.memoryLimit && app.memoryLimit < newConfig.manifest.memoryLimit) {
|
||||
newConfig.memoryLimit = newConfig.manifest.memoryLimit;
|
||||
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
|
||||
if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) {
|
||||
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
|
||||
}
|
||||
|
||||
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { newConfig: newConfig }, function (error) {
|
||||
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { updateConfig: updateConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -762,22 +771,22 @@ function restore(appId, data, auditSource, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// for empty or null backupId, use existing manifest to mimic a reinstall
|
||||
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
|
||||
func(function (error, restoreConfig) {
|
||||
func(function (error, backupInfo) {
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
var values = {
|
||||
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
|
||||
manifest: restoreConfig.manifest,
|
||||
restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls
|
||||
manifest: backupInfo.manifest,
|
||||
|
||||
oldConfig: getAppConfig(app)
|
||||
};
|
||||
@@ -816,24 +825,24 @@ function clone(appId, data, auditSource, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
backups.getRestoreConfig(backupId, function (error, restoreConfig) {
|
||||
backups.get(backupId, function (error, backupInfo) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts);
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest;
|
||||
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest;
|
||||
|
||||
appstore.purchase(newAppId, appStoreId, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
@@ -846,7 +855,7 @@ function clone(appId, data, auditSource, callback) {
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
lastBackupId: backupId,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
|
||||
};
|
||||
@@ -1012,14 +1021,9 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
var newTcpPorts = newManifest.tcpPorts || { };
|
||||
var oldTcpPorts = app.manifest.tcpPorts || { };
|
||||
var portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (var env in newTcpPorts) {
|
||||
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
|
||||
}
|
||||
|
||||
for (env in portBindings) {
|
||||
for (var env in portBindings) {
|
||||
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
|
||||
}
|
||||
|
||||
@@ -1034,7 +1038,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
if (error) {
|
||||
debug('Cannot autoupdate app %s : %s', appId, error.message);
|
||||
return iteratorDone();
|
||||
}
|
||||
}
|
||||
|
||||
error = canAutoupdateApp(app, updateInfo[appId].manifest);
|
||||
if (error) {
|
||||
@@ -1104,10 +1108,14 @@ function restoreInstalledApps(callback) {
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug('marking %s for restore', app.location || app.id);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) {
|
||||
if (error) debug('did not mark %s for restore', app.location || app.id, error);
|
||||
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
|
||||
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
if (error) debug('did not mark %s for restore', app.location || app.id, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -1205,7 +1213,7 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var readFile = fs.createReadStream(sourceFilePath);
|
||||
readFile.on('error', console.error);
|
||||
readFile.on('error', callback);
|
||||
|
||||
readFile.pipe(stream);
|
||||
|
||||
|
||||
+34
-7
@@ -13,6 +13,8 @@ exports = module.exports = {
|
||||
|
||||
getAccount: getAccount,
|
||||
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
@@ -164,16 +166,17 @@ function sendAliveStatus(data, callback) {
|
||||
provider: result[settings.TLS_CONFIG_KEY].provider
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY],
|
||||
@@ -182,6 +185,7 @@ function sendAliveStatus(data, callback) {
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
@@ -265,3 +269,26 @@ function getAccount(callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendFeedback(info, callback) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
assert.strictEqual(typeof info.email, 'string');
|
||||
assert.strictEqual(typeof info.displayName, 'string');
|
||||
assert.strictEqual(typeof info.type, 'string');
|
||||
assert.strictEqual(typeof info.subject, 'string');
|
||||
assert.strictEqual(typeof info.description, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+33
-11
@@ -35,6 +35,7 @@ var addons = require('./addons.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apptask'),
|
||||
docker = require('./docker.js'),
|
||||
ejs = require('ejs'),
|
||||
@@ -388,7 +389,7 @@ function install(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const backupId = app.lastBackupId, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
|
||||
const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for install/restore from
|
||||
@@ -428,7 +429,7 @@ function install(app, callback) {
|
||||
createVolume.bind(null, app),
|
||||
|
||||
function restoreFromBackup(next) {
|
||||
if (!backupId) {
|
||||
if (!restoreConfig) {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
@@ -436,7 +437,7 @@ function install(app, callback) {
|
||||
} else {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -456,7 +457,7 @@ function install(app, callback) {
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
@@ -574,17 +575,17 @@ function update(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, `Updating to ${app.newConfig.manifest.version}`);
|
||||
debugApp(app, `Updating to ${app.updateConfig.manifest.version}`);
|
||||
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.newConfig.manifest.addons));
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for update from
|
||||
// a previous version of box code
|
||||
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
|
||||
verifyManifest.bind(null, app.newConfig.manifest),
|
||||
verifyManifest.bind(null, app.updateConfig.manifest),
|
||||
|
||||
function (next) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
@@ -598,7 +599,7 @@ function update(app, callback) {
|
||||
// download new image before app is stopped. this is so we can reduce downtime
|
||||
// and also not remove the 'common' layers when the old image is deleted
|
||||
updateApp.bind(null, app, { installationProgress: '25, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.newConfig.manifest),
|
||||
docker.downloadImage.bind(null, app.updateConfig.manifest),
|
||||
|
||||
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
|
||||
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
|
||||
@@ -608,7 +609,7 @@ function update(app, callback) {
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.newConfig.manifest.dockerImage) return done();
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
docker.deleteImage(app.manifest, done);
|
||||
},
|
||||
@@ -616,8 +617,29 @@ function update(app, callback) {
|
||||
// only delete unused addons after backup
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
// make sure we always have objects
|
||||
var currentPorts = app.portBindings || {};
|
||||
var newPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
|
||||
async.each(Object.keys(currentPorts), function (portName, callback) {
|
||||
if (newPorts[portName]) return callback(); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
else if (error) return next(error);
|
||||
|
||||
// also delete from app object for further processing (the db is updated in the next step)
|
||||
delete app.portBindings[portName];
|
||||
|
||||
callback();
|
||||
});
|
||||
}, next);
|
||||
},
|
||||
|
||||
// switch over to the new config. manifest, memoryLimit, portBindings, appstoreId are updated here
|
||||
updateApp.bind(null, app, app.newConfig),
|
||||
updateApp.bind(null, app, app.updateConfig),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -639,7 +661,7 @@ function update(app, callback) {
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'updated');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, newConfig: null }, callback);
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
|
||||
+7
-7
@@ -6,7 +6,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'restoreConfigJson', 'format' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -34,8 +34,8 @@ function postProcess(result) {
|
||||
|
||||
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
|
||||
|
||||
result.restoreConfig = result.restoreConfigJson ? safe.JSON.parse(result.restoreConfigJson) : null;
|
||||
delete result.restoreConfigJson;
|
||||
result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null;
|
||||
delete result.manifestJson;
|
||||
}
|
||||
|
||||
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
@@ -109,15 +109,15 @@ function add(backup, callback) {
|
||||
assert.strictEqual(typeof backup.version, 'string');
|
||||
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(backup.dependsOn));
|
||||
assert.strictEqual(typeof backup.restoreConfig, 'object');
|
||||
assert.strictEqual(typeof backup.manifest, 'object');
|
||||
assert.strictEqual(typeof backup.format, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||
var restoreConfig = backup.restoreConfig ? JSON.stringify(backup.restoreConfig) : '';
|
||||
var manifestJson = JSON.stringify(backup.manifest);
|
||||
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, restoreConfigJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), restoreConfig, backup.format ],
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
|
||||
function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
+24
-52
@@ -8,7 +8,7 @@ exports = module.exports = {
|
||||
getByStatePaged: getByStatePaged,
|
||||
getByAppIdPaged: getByAppIdPaged,
|
||||
|
||||
getRestoreConfig: getRestoreConfig,
|
||||
get: get,
|
||||
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
@@ -150,16 +150,15 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getRestoreConfig(backupId, callback) {
|
||||
function get(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backupdb.get(backupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
if (!result.restoreConfig) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
||||
|
||||
callback(null, result.restoreConfig);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,8 +275,8 @@ function saveFsMetadata(appDataDir, callback) {
|
||||
if (execFiles === null) return callback(safe.error);
|
||||
|
||||
var metadata = {
|
||||
emptyDirs: emptyDirs.trim().split('\n'),
|
||||
execFiles: execFiles.trim().split('\n')
|
||||
emptyDirs: emptyDirs.length === 0 ? [ ] : emptyDirs.trim().split('\n'),
|
||||
execFiles: execFiles.length === 0 ? [ ] : execFiles.trim().split('\n')
|
||||
};
|
||||
|
||||
if (!safe.fs.writeFileSync(`${appDataDir}/fsmetadata.json`, JSON.stringify(metadata, null, 4))) return callback(safe.error);
|
||||
@@ -418,29 +417,23 @@ function download(backupId, format, dataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(app, addonsToRestore, backupId, callback) {
|
||||
function restoreApp(app, addonsToRestore, restoreConfig, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof addonsToRestore, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof restoreConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(app.lastBackupId);
|
||||
|
||||
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
backupdb.get(backupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
async.series([
|
||||
download.bind(null, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
|
||||
addons.restoreAddons.bind(null, app, addonsToRestore)
|
||||
], function (error) {
|
||||
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
||||
|
||||
async.series([
|
||||
download.bind(null, backupId, result.format, appDataDir),
|
||||
addons.restoreAddons.bind(null, app, addonsToRestore)
|
||||
], function (error) {
|
||||
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -559,7 +552,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
|
||||
log(`Rotating box backup to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null, format: format }, function (error) {
|
||||
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -616,29 +609,13 @@ function snapshotApp(app, manifest, callback) {
|
||||
|
||||
log(`Snapshotting app ${app.id}`);
|
||||
|
||||
var restoreConfig = apps.getAppConfig(app);
|
||||
restoreConfig.manifest = manifest;
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(restoreConfig))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
||||
}
|
||||
|
||||
addons.backupAddons(app, manifest.addons, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
return callback(null, restoreConfig);
|
||||
});
|
||||
}
|
||||
|
||||
function setRestorePoint(appId, lastBackupId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -653,14 +630,13 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
|
||||
|
||||
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var restoreConfig = snapshotInfo.restoreConfig;
|
||||
var manifest = restoreConfig.manifest;
|
||||
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
|
||||
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
|
||||
const format = backupConfig.format;
|
||||
|
||||
log(`Rotating app backup of ${app.id} to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig, format: format }, function (error) {
|
||||
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -674,11 +650,7 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
|
||||
log(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
|
||||
|
||||
setRestorePoint(app.id, backupId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, backupId);
|
||||
});
|
||||
callback(null, backupId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -694,7 +666,7 @@ function uploadAppSnapshot(backupConfig, app, manifest, callback) {
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
snapshotApp(app, manifest, function (error, restoreConfig) {
|
||||
snapshotApp(app, manifest, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backupId = util.format('snapshot/app_%s', app.id);
|
||||
@@ -704,7 +676,7 @@ function uploadAppSnapshot(backupConfig, app, manifest, callback) {
|
||||
|
||||
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
|
||||
|
||||
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), restoreConfig: restoreConfig, format: backupConfig.format }, callback);
|
||||
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: manifest, format: backupConfig.format }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -769,7 +741,7 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
|
||||
if (!app.enableBackup) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
return iteratorCallback(null, app.lastBackupId); // just use the last backup
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
backupAppWithTimestamp(app, app.manifest, timestamp, function (error, backupId) {
|
||||
@@ -792,12 +764,12 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, filename) {
|
||||
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
|
||||
|
||||
callback(error, filename);
|
||||
callback(error, backupId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ var assert = require('assert'),
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf';
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
+2
-2
@@ -177,7 +177,7 @@ function renewAll(auditSource, callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
|
||||
allApps.push({ location: config.adminLocation() }); // inject fake webadmin app
|
||||
|
||||
var expiringApps = [ ];
|
||||
for (var i = 0; i < allApps.length; i++) {
|
||||
@@ -239,7 +239,7 @@ function renewAll(auditSource, callback) {
|
||||
}
|
||||
|
||||
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
||||
var configureFunc = app.location === constants.ADMIN_LOCATION ?
|
||||
var configureFunc = app.location === config.adminLocation() ?
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||
|
||||
|
||||
+33
-60
@@ -12,7 +12,7 @@ exports = module.exports = {
|
||||
dnsSetup: dnsSetup,
|
||||
getLogs: getLogs,
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
sendCaasHeartbeat: sendCaasHeartbeat,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
reboot: reboot,
|
||||
@@ -126,7 +126,6 @@ function initialize(callback) {
|
||||
async.series([
|
||||
certificates.initialize,
|
||||
settings.initialize,
|
||||
installAppBundle,
|
||||
configureDefaultServer,
|
||||
onDomainConfigured
|
||||
], function (error) {
|
||||
@@ -238,7 +237,7 @@ function configureWebadmin(callback) {
|
||||
function configureNginx(error) {
|
||||
debug('configureNginx: dns update:%j', error);
|
||||
|
||||
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
|
||||
certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return done(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
@@ -417,6 +416,9 @@ function getConfig(callback) {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.getAll(),
|
||||
@@ -437,8 +439,8 @@ function getConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
if (config.provider() !== 'caas') return;
|
||||
function sendCaasHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
@@ -538,9 +540,9 @@ function addDnsRecords(ip, callback) {
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
|
||||
var webadminRecord = { subdomain: config.adminLocation(), type: 'A', values: [ ip ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
@@ -670,19 +672,19 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
retire('upgrade');
|
||||
});
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -706,6 +708,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
tlsCert: config.tlsCert(),
|
||||
tlsKey: config.tlsKey(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
@@ -737,36 +740,6 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function installAppBundle(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback();
|
||||
|
||||
var bundle = config.get('appBundle');
|
||||
debug('initialize: installing app bundle on first run: %j', bundle);
|
||||
|
||||
if (!bundle || bundle.length === 0) return callback();
|
||||
|
||||
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
|
||||
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
|
||||
|
||||
var data = {
|
||||
appStoreId: appInfo.appstoreId,
|
||||
location: appInfo.location,
|
||||
portBindings: appInfo.portBindings || null,
|
||||
accessRestriction: appInfo.accessRestriction || null,
|
||||
};
|
||||
|
||||
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
|
||||
}, function (error) {
|
||||
if (error) debug('autoInstallApps: ', error);
|
||||
|
||||
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
@@ -844,20 +817,20 @@ function doMigrate(options, callback) {
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
|
||||
+69
-35
@@ -17,6 +17,7 @@ exports = module.exports = {
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
fqdn: fqdn,
|
||||
zoneName: zoneName,
|
||||
setFqdn: setFqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
@@ -28,12 +29,14 @@ exports = module.exports = {
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // caas routes
|
||||
adminLocation: adminLocation,
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
setZoneName: setZoneName,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
@@ -45,13 +48,17 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:config.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
|
||||
var data = { };
|
||||
@@ -63,8 +70,25 @@ function baseDir() {
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
// only tests can run without a config file on disk, they use the defaults with runtime overrides
|
||||
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
|
||||
|
||||
function saveSync() {
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
token: data.token,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
fqdn: data.fqdn,
|
||||
zoneName: data.zoneName,
|
||||
adminLocation: data.adminLocation,
|
||||
isCustomDomain: data.isCustomDomain,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function _reset(callback) {
|
||||
@@ -77,45 +101,41 @@ function _reset(callback) {
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = 'localhost';
|
||||
data.fqdn = '';
|
||||
data.zoneName = '';
|
||||
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.token = null;
|
||||
data.version = null;
|
||||
data.isCustomDomain = true;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.smtpPort = 2525; // // this value comes from mail container
|
||||
data.provider = 'caas';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.provider = 'caas';
|
||||
data.appBundle = [ ];
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.database = null;
|
||||
} else if (exports.TEST) {
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
port: 3306,
|
||||
name: 'box'
|
||||
};
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: '',
|
||||
port: 3306,
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
}
|
||||
|
||||
if (safe.fs.existsSync(cloudronConfigFileName)) {
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData); // overwrite defaults with saved config
|
||||
return;
|
||||
}
|
||||
|
||||
saveSync();
|
||||
// overwrite defaults with saved config
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData);
|
||||
}
|
||||
|
||||
initConfig();
|
||||
@@ -176,16 +196,24 @@ function appFqdn(location) {
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(constants.ADMIN_LOCATION);
|
||||
function mailLocation() {
|
||||
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
|
||||
}
|
||||
|
||||
function mailFqdn() {
|
||||
return appFqdn(constants.MAIL_LOCATION);
|
||||
return appFqdn(mailLocation());
|
||||
}
|
||||
|
||||
function adminLocation() {
|
||||
return get('adminLocation');
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
return 'https://' + appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
@@ -237,4 +265,10 @@ function tlsKey() {
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
// default admin installation location. keep in sync with ADMIN_LOCATION in setup/start.sh and BOX_ADMIN_LOCATION in appstore constants.js
|
||||
exports = module.exports = {
|
||||
ADMIN_LOCATION: 'my',
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
|
||||
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
|
||||
|
||||
// These are combined into one array because users and groups become mailboxes
|
||||
@@ -36,8 +33,6 @@ exports = module.exports = {
|
||||
|
||||
DEMO_USERNAME: 'cloudron',
|
||||
|
||||
DKIM_SELECTOR: 'cloudron',
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never'
|
||||
};
|
||||
|
||||
|
||||
+17
-15
@@ -35,7 +35,7 @@ var gAliveJob = null, // send periodic stats
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gDynamicDNSJob = null,
|
||||
gHeartbeatJob = null, // for CaaS health check
|
||||
gCaasHeartbeatJob = null, // for CaaS health check
|
||||
gSchedulerSyncJob = null,
|
||||
gDigestEmailJob = null;
|
||||
|
||||
@@ -53,18 +53,20 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
start: false
|
||||
});
|
||||
// hack: send the first heartbeat only after we are running for 60 seconds
|
||||
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
|
||||
setTimeout(function () {
|
||||
if (!gHeartbeatJob) return; // already uninitalized
|
||||
gHeartbeatJob.start();
|
||||
cloudron.sendHeartbeat();
|
||||
}, 1000 * 60);
|
||||
if (config.provider() === 'caas') {
|
||||
gCaasHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendCaasHeartbeat,
|
||||
start: false
|
||||
});
|
||||
// hack: send the first heartbeat only after we are running for 60 seconds
|
||||
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
|
||||
setTimeout(function () {
|
||||
if (!gCaasHeartbeatJob) return; // already uninitalized
|
||||
gCaasHeartbeatJob.start();
|
||||
cloudron.sendCaasHeartbeat();
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
gAliveJob = new CronJob({
|
||||
@@ -252,8 +254,8 @@ function uninitialize(callback) {
|
||||
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = null;
|
||||
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = null;
|
||||
if (gCaasHeartbeatJob) gCaasHeartbeatJob.stop();
|
||||
gCaasHeartbeatJob = null;
|
||||
|
||||
if (gAliveJob) gAliveJob.stop();
|
||||
gAliveJob = null;
|
||||
|
||||
+1
-21
@@ -7,19 +7,15 @@ exports = module.exports = {
|
||||
|
||||
isEnabled: isEnabled,
|
||||
setEnabled: setEnabled,
|
||||
issueDeveloperToken: issueDeveloperToken,
|
||||
getNonApprovedApps: getNonApprovedApps
|
||||
issueDeveloperToken: issueDeveloperToken
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:developer'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function DeveloperError(reason, errorOrMessage) {
|
||||
@@ -84,19 +80,3 @@ function issueDeveloperToken(user, auditSource, callback) {
|
||||
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
|
||||
});
|
||||
}
|
||||
|
||||
function getNonApprovedApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
||||
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401 || result.statusCode === 403) {
|
||||
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
|
||||
return callback(null, []);
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body.apps || []);
|
||||
});
|
||||
}
|
||||
|
||||
+25
-18
@@ -33,31 +33,38 @@ function maybeSend(callback) {
|
||||
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
|
||||
if (error) return callback(error);
|
||||
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
|
||||
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
|
||||
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
|
||||
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
|
||||
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
|
||||
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
|
||||
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
if (error) return callback(error);
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
|
||||
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
|
||||
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
|
||||
};
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
} else {
|
||||
debug('maybeSend: nothing happened, NOT sending digest email');
|
||||
}
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
|
||||
callback();
|
||||
});
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+48
-48
@@ -10,7 +10,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/digitalocean'),
|
||||
dns = require('dns'),
|
||||
safe = require('safetydance'),
|
||||
@@ -37,22 +37,22 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
var url = nextPage ? nextPage : DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records';
|
||||
|
||||
superagent.get(url)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
}));
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
}));
|
||||
|
||||
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
|
||||
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () { return !!nextPage; }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -98,37 +98,37 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
if (i >= result.length) {
|
||||
superagent.post(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records')
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
}, function (error, id) {
|
||||
if (error) return callback(error);
|
||||
@@ -183,18 +183,18 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
// FIXME we only handle the first one currently
|
||||
|
||||
superagent.del(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + tmp[0].id)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+3
-3
@@ -9,10 +9,10 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/gcdns'),
|
||||
dns = require('dns'),
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -187,7 +187,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/manual'),
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
@@ -58,7 +58,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
|
||||
var adminDomain = config.adminLocation() + '.' + domain;
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
@@ -247,7 +247,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+3
-13
@@ -367,31 +367,21 @@ function getContainerIdByIp(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('get container by ip %s', ip);
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.listNetworks({}, function (error, result) {
|
||||
docker.getNetwork('cloudron').inspect(function (error, bridge) {
|
||||
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
|
||||
if (error) return callback(error);
|
||||
|
||||
var bridge;
|
||||
result.forEach(function (n) {
|
||||
if (n.Name === 'cloudron') bridge = n;
|
||||
});
|
||||
|
||||
if (!bridge) return callback(new Error('Unable to find the cloudron network'));
|
||||
|
||||
var containerId;
|
||||
for (var id in bridge.Containers) {
|
||||
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
|
||||
if (bridge.Containers[id].IPv4Address.indexOf(ip + '/16') === 0) {
|
||||
containerId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!containerId) return callback(new Error('No container with that ip'));
|
||||
|
||||
debug('found container %s with ip %s', containerId, ip);
|
||||
|
||||
callback(null, containerId);
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ function verifyRelay(relay, callback) {
|
||||
|
||||
function checkDkim(callback) {
|
||||
var dkim = {
|
||||
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
|
||||
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
|
||||
+8
-8
@@ -6,7 +6,7 @@ exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
getByCreationTime: getByCreationTime,
|
||||
cleanup: cleanup,
|
||||
|
||||
// keep in sync with webadmin index.js filter and CLI tool
|
||||
@@ -98,21 +98,21 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
callback(null, events);
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
function getByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
|
||||
eventlogdb.getByCreationTime(creationTime, function (error, events) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
callback(null, events);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function cleanup(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 7); // 7 days ago
|
||||
d.setDate(d.getDate() - 10); // 10 days ago
|
||||
|
||||
// only cleanup high frequency events
|
||||
var actions = [
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
getByCreationTime: getByCreationTime,
|
||||
add: add,
|
||||
count: count,
|
||||
delByCreationTime: delByCreationTime,
|
||||
@@ -73,12 +73,12 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
function getByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE action=? AND creationTime >= DATE_SUB(NOW(), INTERVAL 1 WEEK) ORDER BY creationTime DESC';
|
||||
database.query(query, [ action ], function (error, results) {
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
|
||||
database.query(query, [ creationTime ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.6.0',
|
||||
'version': '48.8.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
@@ -15,10 +15,10 @@ exports = module.exports = {
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.37.3' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+123
-15
@@ -61,12 +61,74 @@ function getUsersWithAccessToApp(req, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// helper function to deal with pagination
|
||||
function finalSend(results, req, res, next) {
|
||||
var min = 0;
|
||||
var max = results.length;
|
||||
var cookie = null;
|
||||
var pageSize = 0;
|
||||
|
||||
// check if this is a paging request, if so get the cookie for session info
|
||||
req.controls.forEach(function (control) {
|
||||
if (control.type === ldap.PagedResultsControl.OID) {
|
||||
pageSize = control.value.size;
|
||||
cookie = control.value.cookie;
|
||||
}
|
||||
});
|
||||
|
||||
function sendPagedResults(start, end) {
|
||||
start = (start < min) ? min : start;
|
||||
end = (end > max || end < min) ? max : end;
|
||||
var i;
|
||||
|
||||
for (i = start; i < end; i++) {
|
||||
res.send(results[i]);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
if (cookie && Buffer.isBuffer(cookie)) {
|
||||
// we have pagination
|
||||
var first = min;
|
||||
if (cookie.length !== 0) {
|
||||
first = parseInt(cookie.toString(), 10);
|
||||
}
|
||||
var last = sendPagedResults(first, first + pageSize);
|
||||
|
||||
var resultCookie;
|
||||
if (last < max) {
|
||||
resultCookie = new Buffer(last.toString());
|
||||
} else {
|
||||
resultCookie = new Buffer('');
|
||||
}
|
||||
|
||||
res.controls.push(new ldap.PagedResultsControl({
|
||||
value: {
|
||||
size: pageSize, // correctness not required here
|
||||
cookie: resultCookie
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// no pagination simply send all
|
||||
results.forEach(function (result) {
|
||||
res.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
// all done
|
||||
res.end();
|
||||
next();
|
||||
}
|
||||
|
||||
function userSearch(req, res, next) {
|
||||
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
var results = [];
|
||||
|
||||
// send user objects
|
||||
result.forEach(function (entry) {
|
||||
// skip entries with empty username. Some apps like owncloud can't deal with this
|
||||
@@ -109,11 +171,11 @@ function userSearch(req, res, next) {
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
finalSend(results, req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,6 +185,8 @@ function groupSearch(req, res, next) {
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
var results = [];
|
||||
|
||||
var groups = [{
|
||||
name: 'users',
|
||||
admin: false
|
||||
@@ -149,11 +213,43 @@ function groupSearch(req, res, next) {
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
finalSend(results, req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
function groupUsersCompare(req, res, next) {
|
||||
debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
});
|
||||
}
|
||||
|
||||
function groupAdminsCompare(req, res, next) {
|
||||
debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && found.admin) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,6 +257,7 @@ function mailboxSearch(req, res, next) {
|
||||
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
// allow login via email
|
||||
var parts = name.split('@');
|
||||
@@ -188,9 +285,11 @@ function mailboxSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,6 +297,7 @@ function mailAliasSearch(req, res, next) {
|
||||
debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, alias) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
@@ -218,9 +318,11 @@ function mailAliasSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,6 +330,7 @@ function mailingListSearch(req, res, next) {
|
||||
debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, group) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
@@ -248,9 +351,11 @@ function mailingListSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,14 +475,17 @@ function start(callback) {
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
|
||||
debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id
|
||||
res.end();
|
||||
});
|
||||
|
||||
// this is the bind for apps (after bind, they might search and authenticate user)
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) {
|
||||
// TODO: validate password
|
||||
debug('application bind: %s', req.dn.toString());
|
||||
res.end();
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
Dear Cloudron Admin,
|
||||
|
||||
a new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
Changes:
|
||||
<%= updateInfo.manifest.changelog %>
|
||||
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
@@ -16,4 +17,35 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- changelogHTML %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -4,15 +4,18 @@ Dear <%= cloudronName %> Admin,
|
||||
|
||||
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
|
||||
|
||||
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
* <%- changelog[i] %>
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
your Cloudron
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
@@ -27,11 +30,6 @@ your Cloudron
|
||||
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Your Cloudron will update automatically tonight.<br/>
|
||||
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<ul>
|
||||
<% for (var i = 0; i < changelogHTML.length; i++) { %>
|
||||
@@ -40,6 +38,11 @@ your Cloudron
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
|
||||
This is a summary of the activities on your Cloudron <%= fqdn %>.
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
|
||||
The following users were added:
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { -%>
|
||||
* <%- info.usersAdded[i].email %>
|
||||
<% }} -%>
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
|
||||
The certificates of the following apps was renewed:
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { -%>
|
||||
* <%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %>
|
||||
<% }} -%>
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
|
||||
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
|
||||
@@ -33,6 +45,14 @@ The following apps were updated:
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedBackups.length) { -%>
|
||||
|
||||
Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBackups[0].filename %>
|
||||
<% } else { -%>
|
||||
|
||||
This Cloudron did **not** backup successfully in the last week!
|
||||
<% } -%>
|
||||
|
||||
<% if (!info.hasSubscription) { -%>
|
||||
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
@@ -52,9 +72,25 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>Weekly summary of activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a>:</p>
|
||||
<p>This is a summary of the activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a> last week.</p>
|
||||
|
||||
<br/>
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
<p><b>The following users were added:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { %>
|
||||
<li><%- info.usersAdded[i].email %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
<p><b>The certificates of the following apps were renewed:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { %>
|
||||
<li><%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
<p><b>Cloudron v<%- info.pendingBoxUpdate.version %> is available:</b></p>
|
||||
@@ -113,6 +149,12 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedBackups.length) { %>
|
||||
<p><b>Last successful backup : </b> <%= info.finishedBackups[0].backupId || info.finishedBackups[0].filename %> </p>
|
||||
<% } else { %>
|
||||
<p><b>This Cloudron did not backup successfully in the last week!</b></p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!info.hasSubscription) { %>
|
||||
@@ -123,12 +165,12 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<p style="text-align: right;">
|
||||
<center>
|
||||
<small>
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a><br/>
|
||||
Sent on <%= new Date().toUTCString() %>
|
||||
</small>
|
||||
</p>
|
||||
</center>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
New <%= type %> from <%= fqdn %>.
|
||||
|
||||
Sender: <%= user.email %>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
Subject: <%= subject %>
|
||||
-----------------------------------------------------------
|
||||
<%= description %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
+2
-1
@@ -134,7 +134,8 @@ function getGroup(name, callback) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ?', [ results[0].ownerId ], function (error, memberList) {
|
||||
// username can be null if the user has not signed up with the invite yet
|
||||
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ? AND users.username IS NOT NULL', [ results[0].ownerId ], function (error, memberList) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results[0].members = memberList.map(function (m) { return m.username; });
|
||||
|
||||
+78
-83
@@ -23,22 +23,13 @@ exports = module.exports = {
|
||||
|
||||
certificateRenewalError: certificateRenewalError,
|
||||
|
||||
FEEDBACK_TYPE_FEEDBACK: 'feedback',
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
|
||||
FEEDBACK_TYPE_APP_ERROR: 'app_error',
|
||||
FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request',
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
_clearMailQueue: _clearMailQueue
|
||||
};
|
||||
|
||||
var appstore = require('./appstore.js'),
|
||||
AppstoreError = appstore.AppstoreError,
|
||||
assert = require('assert'),
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
@@ -171,6 +162,7 @@ function getAdminEmails(callback) {
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
@@ -244,7 +236,7 @@ function userAdded(user, inviteSent) {
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
@@ -341,7 +333,7 @@ function appDied(app) {
|
||||
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -354,12 +346,13 @@ function appDied(app) {
|
||||
});
|
||||
}
|
||||
|
||||
function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof newBoxVersion, 'string');
|
||||
assert(util.isArray(changelog));
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
@@ -373,6 +366,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
newBoxVersion: newBoxVersion,
|
||||
hasSubscription: hasSubscription,
|
||||
changelog: changelog,
|
||||
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
|
||||
cloudronName: cloudronName,
|
||||
@@ -385,7 +379,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', config.fqdn()),
|
||||
@@ -398,29 +392,13 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
});
|
||||
}
|
||||
|
||||
function appUpdateAvailable(app, updateInfo) {
|
||||
function appUpdateAvailable(app, hasSubscription, info) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Update available for %s', config.fqdn(), app.fqdn),
|
||||
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
@@ -428,34 +406,73 @@ function sendDigest(info) {
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
appstore.getAccount(function (error, appstoreProfile) {
|
||||
if (error && error.reason !== AppstoreError.BILLING_REQUIRED) console.error(error);
|
||||
if (appstoreProfile) adminEmails.push(appstoreProfile.email);
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
};
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
app: app,
|
||||
updateInfo: info,
|
||||
changelogHTML: converter.makeHtml(info.manifest.changelog),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('App %s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', templateDataText),
|
||||
html: render('app_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -464,7 +481,7 @@ function outOfDiskSpace(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -481,7 +498,7 @@ function backupFailed(error) {
|
||||
var message = splatchError(error);
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -499,7 +516,7 @@ function certificateRenewalError(domain, message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -517,7 +534,7 @@ function oomEvent(program, context) {
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -549,28 +566,6 @@ function unexpectedExit(program, context, callback) {
|
||||
sendMails([ mailOptions ], callback);
|
||||
}
|
||||
|
||||
function sendFeedback(user, type, subject, description) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof subject, 'string');
|
||||
assert.strictEqual(typeof description, 'string');
|
||||
|
||||
assert(type === exports.FEEDBACK_TYPE_TICKET ||
|
||||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
|
||||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
|
||||
type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST ||
|
||||
type === exports.FEEDBACK_TYPE_APP_ERROR);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: 'support@cloudron.io',
|
||||
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
|
||||
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
function sendTestMail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
|
||||
@@ -31,6 +31,5 @@ exports = module.exports = {
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
FIRST_RUN_FILE: path.join(config.baseDir(), 'boxdata/first_run'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json')
|
||||
};
|
||||
|
||||
+1
-1
@@ -254,7 +254,7 @@ function createMailConfig(callback) {
|
||||
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
|
||||
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
|
||||
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -52,7 +52,6 @@ function removeInternalAppFields(app) {
|
||||
health: app.health,
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
lastBackupId: app.lastBackupId,
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
@@ -121,6 +120,7 @@ function installApp(req, res, next) {
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
|
||||
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
|
||||
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
|
||||
|
||||
// falsy values in cert and key unset the cert
|
||||
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
@@ -313,11 +313,10 @@ function updateApp(req, res, next) {
|
||||
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
|
||||
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
|
||||
|
||||
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
apps.update(req.params.id, req.body, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
|
||||
+27
-22
@@ -19,7 +19,9 @@ exports = module.exports = {
|
||||
sendTestMail: sendTestMail
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = require('../appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
@@ -66,13 +68,13 @@ function activate(req, res, next) {
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,15 +102,15 @@ function setupTokenAuth(req, res, next) {
|
||||
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next();
|
||||
});
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
@@ -224,17 +226,20 @@ function checkForUpdates(req, res, next) {
|
||||
function feedback(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_TICKET &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"'));
|
||||
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
|
||||
|
||||
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
|
||||
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
|
||||
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
|
||||
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
|
||||
|
||||
mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description);
|
||||
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
|
||||
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
|
||||
@@ -4,8 +4,7 @@ exports = module.exports = {
|
||||
enabled: enabled,
|
||||
setEnabled: setEnabled,
|
||||
status: status,
|
||||
login: login,
|
||||
apps: apps
|
||||
login: login
|
||||
};
|
||||
|
||||
var developer = require('../developer.js'),
|
||||
@@ -52,9 +51,3 @@ function login(req, res, next) {
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
function apps(req, res, next) {
|
||||
developer.getNonApprovedApps(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { apps: result }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
|
||||
hock = require('hock'),
|
||||
http = require('http'),
|
||||
https = require('https'),
|
||||
js2xml = require('js2xmlparser'),
|
||||
js2xml = require('js2xmlparser').parse,
|
||||
ldap = require('../../ldap.js'),
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
@@ -344,10 +344,10 @@ describe('App API', function () {
|
||||
it('app install fails - reserved admin location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, location: constants.ADMIN_LOCATION, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
|
||||
expect(res.body.message).to.eql('my is reserved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -648,7 +648,7 @@ describe('App installation', function () {
|
||||
awsHockInstance
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.max(Infinity)
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }), { 'Content-Type': 'application/xml' })
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
|
||||
.filteringPathRegEx(/name=[^&]*/, 'name=location')
|
||||
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
|
||||
.max(Infinity)
|
||||
|
||||
@@ -189,8 +189,6 @@ describe('Cloudron', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
@@ -484,8 +482,6 @@ describe('Cloudron', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
@@ -544,26 +540,6 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
@@ -594,16 +570,6 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with feedback type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
@@ -613,6 +579,42 @@ describe('Cloudron', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs', function () {
|
||||
@@ -624,8 +626,6 @@ describe('Cloudron', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('REST API', function () {
|
||||
.send("some invalid non-strict json")
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(result.body.message).to.be('Bad JSON');
|
||||
expect(result.body.message).to.be('Failed to parse body');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+25
-26
@@ -59,26 +59,26 @@ function initializeExpressSync() {
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
|
||||
.use(middleware.session({
|
||||
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.BOX_ENV !== 'test',
|
||||
maxAge: 600000
|
||||
}
|
||||
}))
|
||||
.use(passport.initialize())
|
||||
.use(passport.session())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
|
||||
.use(middleware.session({
|
||||
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.BOX_ENV !== 'test',
|
||||
maxAge: 600000
|
||||
}
|
||||
}))
|
||||
.use(passport.initialize())
|
||||
.use(passport.session())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// NOTE: these limits have to be in sync with nginx limits
|
||||
var FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
|
||||
@@ -108,7 +108,6 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
|
||||
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
|
||||
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
|
||||
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
|
||||
|
||||
// cloudron routes
|
||||
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
|
||||
@@ -284,11 +283,11 @@ function initializeSysadminExpressSync() {
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// Sysadmin routes
|
||||
router.post('/api/v1/backup', routes.sysadmin.backup);
|
||||
|
||||
@@ -33,9 +33,6 @@ exports = module.exports = {
|
||||
getTlsConfig: getTlsConfig,
|
||||
setTlsConfig: setTlsConfig,
|
||||
|
||||
getUpdateConfig: getUpdateConfig,
|
||||
setUpdateConfig: setUpdateConfig,
|
||||
|
||||
getAppstoreConfig: getAppstoreConfig,
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
@@ -429,30 +426,6 @@ function setBackupConfig(backupConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUpdateConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.UPDATE_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.UPDATE_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // { prerelease }
|
||||
});
|
||||
}
|
||||
|
||||
function setUpdateConfig(updateConfig, callback) {
|
||||
assert.strictEqual(typeof updateConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.UPDATE_CONFIG_KEY, JSON.stringify(updateConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.UPDATE_CONFIG_KEY, updateConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getMailConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
@@ -107,7 +107,8 @@ function copy(apiConfig, oldFilePath, newFilePath) {
|
||||
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
// this will hardlink backups saving space
|
||||
shell.exec('copy', '/bin/cp', [ '-al', oldFilePath, newFilePath ], { }, function (error) {
|
||||
var cpOptions = apiConfig.noHardlinks ? '-a' : '-al';
|
||||
shell.exec('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
|
||||
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
events.emit('done', null);
|
||||
@@ -159,15 +160,18 @@ function testConfig(apiConfig, callback) {
|
||||
|
||||
if (!apiConfig.backupFolder) return callback(new BackupsError(BackupsError.BAD_FIELD, 'backupFolder is required'));
|
||||
|
||||
fs.stat(apiConfig.backupFolder, function (error, result) {
|
||||
if (error) {
|
||||
debug('testConfig: %s', apiConfig.backupFolder, error);
|
||||
return callback(new BackupsError(BackupsError.BAD_FIELD, 'Directory does not exist or cannot be accessed'));
|
||||
}
|
||||
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'noHardlinks must be boolean'));
|
||||
|
||||
fs.stat(apiConfig.backupFolder, function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + error.message));
|
||||
if (!result.isDirectory()) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Backup location is not a directory'));
|
||||
|
||||
callback(null);
|
||||
mkdirp(path.join(apiConfig.backupFolder, 'snapshot'), function (error) {
|
||||
if (error && error.code === 'EACCES') return callback(new BackupsError(BackupsError.BAD_FIELD, `Access denied. Run "chown yellowtent:yellowtent ${apiConfig.backupFolder}" on the server`));
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, error.message));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+14
-12
@@ -108,7 +108,7 @@ function getS3Config(apiConfig, callback) {
|
||||
|
||||
if (apiConfig.endpoint) credentials.endpoint = apiConfig.endpoint;
|
||||
|
||||
if (apiConfig.acceptSelfSignedCerts === true) {
|
||||
if (apiConfig.acceptSelfSignedCerts === true && credentials.endpoint && credentials.endpoint.startsWith('https://')) {
|
||||
credentials.httpOptions.agent = {
|
||||
agent: new https.Agent({ rejectUnauthorized: false })
|
||||
};
|
||||
@@ -190,7 +190,7 @@ function download(apiConfig, backupFilePath, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listDir(apiConfig, backupFilePath, batchSize, iteratorCallback, callback) {
|
||||
function listDir(apiConfig, backupFilePath, iteratorCallback, callback) {
|
||||
getS3Config(apiConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -207,10 +207,9 @@ function listDir(apiConfig, backupFilePath, batchSize, iteratorCallback, callbac
|
||||
return foreverCallback(error);
|
||||
}
|
||||
|
||||
var arr = batchSize === 1 ? listData.Contents : chunk(listData.Contents, batchSize);
|
||||
if (arr.length === 0) return foreverCallback(new Error('Done'));
|
||||
if (listData.Contents.length === 0) return foreverCallback(new Error('Done'));
|
||||
|
||||
iteratorCallback(s3, arr, function (error) {
|
||||
iteratorCallback(s3, listData.Contents, function (error) {
|
||||
if (error) return foreverCallback(error);
|
||||
|
||||
if (!listData.IsTruncated) return foreverCallback(new Error('Done'));
|
||||
@@ -262,9 +261,9 @@ function downloadDir(apiConfig, backupFilePath, destDir) {
|
||||
});
|
||||
}
|
||||
|
||||
const concurrency = 10, batchSize = 1;
|
||||
const concurrency = 10;
|
||||
|
||||
listDir(apiConfig, backupFilePath, batchSize, function (s3, objects, done) {
|
||||
listDir(apiConfig, backupFilePath, function (s3, objects, done) {
|
||||
total += objects.length;
|
||||
async.eachLimit(objects, concurrency, downloadFile.bind(null, s3), done);
|
||||
}, function (error) {
|
||||
@@ -370,10 +369,9 @@ function copy(apiConfig, oldFilePath, newFilePath) {
|
||||
});
|
||||
}
|
||||
|
||||
const batchSize = 1;
|
||||
var total = 0, concurrency = 4;
|
||||
|
||||
listDir(apiConfig, oldFilePath, batchSize, function (s3, objects, done) {
|
||||
listDir(apiConfig, oldFilePath, function (s3, objects, done) {
|
||||
total += objects.length;
|
||||
|
||||
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
|
||||
@@ -429,7 +427,6 @@ function removeDir(apiConfig, pathPrefix) {
|
||||
Objects: contents.map(function (c) { return { Key: c.Key }; })
|
||||
}
|
||||
};
|
||||
total += contents.length;
|
||||
|
||||
events.emit('progress', `Removing ${contents.length} files from ${contents[0].Key} to ${contents[contents.length-1].Key}`);
|
||||
|
||||
@@ -443,9 +440,14 @@ function removeDir(apiConfig, pathPrefix) {
|
||||
});
|
||||
}
|
||||
|
||||
const batchSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle requests per second
|
||||
listDir(apiConfig, pathPrefix, function (s3, objects, done) {
|
||||
total += objects.length;
|
||||
|
||||
listDir(apiConfig, pathPrefix, batchSize, deleteFiles, function (error) {
|
||||
const batchSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
|
||||
var chunks = batchSize === 1 ? objects : chunk(objects, batchSize);
|
||||
|
||||
async.eachSeries(chunks, deleteFiles.bind(null, s3), done);
|
||||
}, function (error) {
|
||||
events.emit('progress', `Removed ${total} files`);
|
||||
|
||||
events.emit('done', error);
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('Apps', function () {
|
||||
|
||||
describe('validateHostname', function () {
|
||||
it('does not allow admin subdomain', function () {
|
||||
expect(apps._validateHostname(constants.ADMIN_LOCATION, 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('my', 'cloudron.us')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('cannot have >63 length subdomains', function () {
|
||||
@@ -325,11 +325,13 @@ describe('Apps', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with admin not being special', function (done) {
|
||||
it('returns all apps for admin', function (done) {
|
||||
apps.getAllByUser(ADMIN_0, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.length).to.equal(1);
|
||||
expect(result.length).to.equal(3);
|
||||
expect(result[0].id).to.equal(APP_0.id);
|
||||
expect(result[1].id).to.equal(APP_1.id);
|
||||
expect(result[2].id).to.equal(APP_2.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ var addons = require('../addons.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
js2xml = require('js2xmlparser'),
|
||||
js2xml = require('js2xmlparser').parse,
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
@@ -224,7 +224,7 @@ describe('apptask', function () {
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.times(2)
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
|
||||
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + config.fqdn() + '.&type=A')
|
||||
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
|
||||
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
|
||||
@@ -242,7 +242,7 @@ describe('apptask', function () {
|
||||
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
|
||||
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
|
||||
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
|
||||
|
||||
|
||||
@@ -92,14 +92,17 @@ function createBackup(callback) {
|
||||
|
||||
describe('backups', function () {
|
||||
before(function (done) {
|
||||
const BACKUP_DIR = path.join(os.tmpdir(), 'cloudron-backup-test');
|
||||
|
||||
async.series([
|
||||
mkdirp.bind(null, BACKUP_DIR),
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.initialize,
|
||||
settings.setBackupConfig.bind(null, {
|
||||
provider: 'filesystem',
|
||||
key: 'enckey',
|
||||
backupFolder: '/var/backups',
|
||||
backupFolder: BACKUP_DIR,
|
||||
retentionSecs: 1,
|
||||
format: 'tgz'
|
||||
})
|
||||
@@ -121,7 +124,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'backup-app-00', 'backup-app-01' ],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -130,7 +133,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -139,7 +142,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -148,7 +151,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'backup-app-10', 'backup-app-11' ],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -157,7 +160,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -166,7 +169,7 @@ describe('backups', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
|
||||
+11
-12
@@ -6,7 +6,6 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
@@ -25,11 +24,6 @@ describe('config', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('cloudron.conf generated automatically', function (done) {
|
||||
expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get and set version', function (done) {
|
||||
config.setVersion('1.2.3');
|
||||
expect(config.version()).to.be('1.2.3');
|
||||
@@ -38,15 +32,20 @@ describe('config', function () {
|
||||
|
||||
it('did set default values', function () {
|
||||
expect(config.isCustomDomain()).to.equal(true);
|
||||
expect(config.fqdn()).to.equal('localhost');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '.localhost');
|
||||
expect(config.appFqdn('app')).to.equal('app.localhost');
|
||||
expect(config.fqdn()).to.equal('');
|
||||
expect(config.zoneName()).to.equal('');
|
||||
expect(config.adminLocation()).to.equal('my');
|
||||
});
|
||||
|
||||
it('set saves value in file', function (done) {
|
||||
config.set('fqdn', 'example.com');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).fqdn).to.eql('example.com');
|
||||
done();
|
||||
});
|
||||
|
||||
it('set does not save custom values in file', function (done) {
|
||||
config.set('foobar', 'somevalue');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.eql('somevalue');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -68,7 +67,7 @@ describe('config', function () {
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(true);
|
||||
expect(config.fqdn()).to.equal('example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '.example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://my.example.com');
|
||||
expect(config.appFqdn('app')).to.equal('app.example.com');
|
||||
expect(config.zoneName()).to.equal('example.com');
|
||||
});
|
||||
@@ -79,7 +78,7 @@ describe('config', function () {
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(false);
|
||||
expect(config.fqdn()).to.equal('test.example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '-test.example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://my-test.example.com');
|
||||
expect(config.appFqdn('app')).to.equal('app-test.example.com');
|
||||
expect(config.zoneName()).to.equal('example.com');
|
||||
});
|
||||
|
||||
+10
-10
@@ -536,9 +536,9 @@ describe('database', function () {
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
lastBackupId: null,
|
||||
restoreConfig: null,
|
||||
oldConfig: null,
|
||||
newConfig: null,
|
||||
updateConfig: null,
|
||||
memoryLimit: 4294967296,
|
||||
altDomain: null,
|
||||
xFrameOptions: 'DENY',
|
||||
@@ -561,9 +561,9 @@ describe('database', function () {
|
||||
portBindings: { },
|
||||
health: null,
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
lastBackupId: null,
|
||||
restoreConfig: null,
|
||||
oldConfig: null,
|
||||
newConfig: null,
|
||||
updateConfig: null,
|
||||
memoryLimit: 0,
|
||||
altDomain: null,
|
||||
xFrameOptions: 'SAMEORIGIN',
|
||||
@@ -1027,7 +1027,7 @@ describe('database', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'dep1' ],
|
||||
restoreConfig: null,
|
||||
manifest: null,
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -1044,7 +1044,7 @@ describe('database', function () {
|
||||
expect(result.type).to.be(backupdb.BACKUP_TYPE_BOX);
|
||||
expect(result.creationTime).to.be.a(Date);
|
||||
expect(result.dependsOn).to.eql(['dep1']);
|
||||
expect(result.restoreConfig).to.eql(null);
|
||||
expect(result.manifest).to.eql(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1067,7 +1067,7 @@ describe('database', function () {
|
||||
expect(results[0].id).to.be('backup-box');
|
||||
expect(results[0].version).to.be('1.0.0');
|
||||
expect(results[0].dependsOn).to.eql(['dep1']);
|
||||
expect(results[0].restoreConfig).to.eql(null);
|
||||
expect(results[0].manifest).to.eql(null);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -1093,7 +1093,7 @@ describe('database', function () {
|
||||
version: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [ ],
|
||||
restoreConfig: { manifest: { foo: 'bar' } },
|
||||
manifest: { foo: 'bar' },
|
||||
format: 'tgz'
|
||||
};
|
||||
|
||||
@@ -1110,7 +1110,7 @@ describe('database', function () {
|
||||
expect(result.type).to.be(backupdb.BACKUP_TYPE_APP);
|
||||
expect(result.creationTime).to.be.a(Date);
|
||||
expect(result.dependsOn).to.eql([]);
|
||||
expect(result.restoreConfig).to.eql({ manifest: { foo: 'bar' } });
|
||||
expect(result.manifest).to.eql({ foo: 'bar' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1124,7 +1124,7 @@ describe('database', function () {
|
||||
expect(results[0].id).to.be('app_appid_123');
|
||||
expect(results[0].version).to.be('1.0.0');
|
||||
expect(results[0].dependsOn).to.eql([]);
|
||||
expect(results[0].restoreConfig).to.eql({ manifest: { foo: 'bar' } });
|
||||
expect(results[0].manifest).to.eql({ foo: 'bar' });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
+11
-28
@@ -12,10 +12,10 @@ var async = require('async'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('../settings.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
updatechecker = require('../updatechecker.js'),
|
||||
user = require('../user.js');
|
||||
|
||||
@@ -36,8 +36,8 @@ function checkMails(number, email, done) {
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
|
||||
if (number && email) {
|
||||
expect(mailer._getMailQueue()[0].to.indexOf(email)).to.not.equal(-1);
|
||||
if (number) {
|
||||
expect(mailer._getMailQueue()[0].to).to.equal(email);
|
||||
}
|
||||
|
||||
mailer._clearMailQueue();
|
||||
@@ -58,6 +58,7 @@ describe('digest', function () {
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.set('fqdn', 'domain.com');
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
config.set('provider', 'notcaas');
|
||||
@@ -69,6 +70,7 @@ describe('digest', function () {
|
||||
settings.initialize,
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
eventlog.add.bind(null, eventlog.ACTION_UPDATE, AUDIT_SOURCE, { boxUpdateInfo: { sourceTarballUrl: 'xx', version: '1.2.3', changelog: [ 'good stuff' ] } }),
|
||||
settingsdb.set.bind(null, settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true })),
|
||||
mailer.start,
|
||||
mailer._clearMailQueue
|
||||
], done);
|
||||
@@ -84,7 +86,7 @@ describe('digest', function () {
|
||||
it('does not send mail with digest disabled', function (done) {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
checkMails(0, '', done);
|
||||
checkMails(0, null, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +101,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, '', done);
|
||||
checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,39 +111,20 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, '', done);
|
||||
checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('sends mail for pending update to appstore account email (caas)', function (done) {
|
||||
var subscription = {
|
||||
id: 'caas',
|
||||
created: 0,
|
||||
canceled_at: 0,
|
||||
status: 'active',
|
||||
plan: { id: 'caas' }
|
||||
};
|
||||
|
||||
it('sends mail for pending update to owner account email', function (done) {
|
||||
updatechecker._setUpdateInfo({ box: null, apps: { 'appid': { manifest: { version: '1.2.5', changelog: 'noop\nreally' } } } });
|
||||
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/test-user/cloudrons') >= 0; }).reply(201, { cloudron: { id: 'test-cloudron' }});
|
||||
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/test-user/cloudrons/test-cloudron/subscription') >= 0; }).reply(200, { subscription: subscription });
|
||||
var fake3 = nock(config.apiServerOrigin()).get('/api/v1/users/test-user?accessToken=test-token').reply(200, { profile: { id: 'test-user', email: 'test@email.com' } });
|
||||
|
||||
settings.setAppstoreConfig({ userId: 'test-user', token: 'test-token', cloudronId: 'test-cloudron' }, function (error) {
|
||||
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, 'test@email.com', function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
expect(fake1.isDone()).to.be.ok();
|
||||
expect(fake2.isDone()).to.be.ok();
|
||||
expect(fake3.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+25
-3
@@ -36,6 +36,19 @@ var USER_0 = {
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var USER_1 = { // this user has not signed up yet
|
||||
id: 'uuid222',
|
||||
username: null,
|
||||
password: '',
|
||||
email: 'safe2@me.com',
|
||||
admin: false,
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
@@ -167,7 +180,8 @@ describe('Group membership', function () {
|
||||
next();
|
||||
});
|
||||
},
|
||||
userdb.add.bind(null, USER_0.id, USER_0)
|
||||
userdb.add.bind(null, USER_0.id, USER_0),
|
||||
userdb.add.bind(null, USER_1.id, USER_1)
|
||||
], done);
|
||||
});
|
||||
after(cleanup);
|
||||
@@ -201,6 +215,13 @@ describe('Group membership', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can add member without username', function (done) {
|
||||
groups.addMember(group0Object.id, USER_1.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('isMember returns true', function (done) {
|
||||
groups.isMember(group0Object.id, USER_0.id, function (error, member) {
|
||||
expect(error).to.be(null);
|
||||
@@ -212,8 +233,9 @@ describe('Group membership', function () {
|
||||
it('can get members', function (done) {
|
||||
groups.getMembers(group0Object.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.length).to.be(1);
|
||||
expect(result.length).to.be(2);
|
||||
expect(result[0]).to.be(USER_0.id);
|
||||
expect(result[1]).to.be(USER_1.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -224,7 +246,7 @@ describe('Group membership', function () {
|
||||
expect(result.name).to.be(GROUP0_NAME.toLowerCase());
|
||||
expect(result.ownerType).to.be(mailboxdb.TYPE_GROUP);
|
||||
expect(result.ownerId).to.be(group0Object.id);
|
||||
expect(result.members).to.eql([ USER_0.username ]);
|
||||
expect(result.members).to.eql([ USER_0.username ]); // filters out users that have not signed up yet
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+94
-28
@@ -66,7 +66,7 @@ var APP_0 = {
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
lastBackupId: null,
|
||||
restoreConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296
|
||||
};
|
||||
@@ -141,35 +141,33 @@ function setup(done) {
|
||||
var answer = {};
|
||||
var status = 500;
|
||||
|
||||
if (req.method === 'GET' && req.url === '/networks') {
|
||||
answer = [{
|
||||
Name: "irrelevant"
|
||||
}, {
|
||||
Name: "cloudron",
|
||||
Id: "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566",
|
||||
Scope: "local",
|
||||
Driver: "bridge",
|
||||
if (req.method === 'GET' && req.url === '/networks/cloudron') {
|
||||
answer = {
|
||||
Name: 'cloudron',
|
||||
Id: 'f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566',
|
||||
Scope: 'local',
|
||||
Driver: 'bridge',
|
||||
IPAM: {
|
||||
Driver: "default",
|
||||
Driver: 'default',
|
||||
Config: [{
|
||||
Subnet: "172.18.0.0/16"
|
||||
Subnet: '172.18.0.0/16'
|
||||
}]
|
||||
},
|
||||
"Containers": {
|
||||
'Containers': {
|
||||
someOtherContainerId: {
|
||||
"EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda",
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"IPv4Address": "127.0.0.2/16",
|
||||
"IPv6Address": ""
|
||||
'EndpointID': 'ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda',
|
||||
'MacAddress': '02:42:ac:11:00:02',
|
||||
'IPv4Address': '127.0.0.2/16',
|
||||
'IPv6Address': ''
|
||||
},
|
||||
someContainerId: {
|
||||
"EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda",
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"IPv4Address": "127.0.0.1/16",
|
||||
"IPv6Address": ""
|
||||
'EndpointID': 'ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda',
|
||||
'MacAddress': '02:42:ac:11:00:02',
|
||||
'IPv4Address': '127.0.0.1/16',
|
||||
'IPv6Address': ''
|
||||
}
|
||||
}
|
||||
}];
|
||||
};
|
||||
status = 200;
|
||||
}
|
||||
|
||||
@@ -268,10 +266,10 @@ describe('Ldap', function () {
|
||||
it('fails with accessRestriction denied', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
appdb.update(APP_0.id, { accessRestriction: { users: [ USER_1.id ], groups: [] }}, function (error) {
|
||||
appdb.update(APP_0.id, { accessRestriction: { users: [ USER_0.id ], groups: [] }}, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
client.bind('cn=' + USER_0.id + ',ou=users,dc=cloudron', USER_0.password, function (error) {
|
||||
client.bind('cn=' + USER_1.id + ',ou=users,dc=cloudron', USER_1.password, function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
@@ -342,6 +340,35 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with pagination', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: 'objectcategory=person',
|
||||
paged: true
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
entries.sort(function (a, b) { return a.username > b.username; });
|
||||
expect(entries[0].username).to.equal(USER_0.username.toLowerCase());
|
||||
expect(entries[0].mail).to.equal(USER_0.email.toLowerCase());
|
||||
expect(entries[1].username).to.equal(USER_1.username.toLowerCase());
|
||||
expect(entries[1].mail).to.equal(USER_1.email.toLowerCase());
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with basic filter and email enabled', function (done) {
|
||||
// user settingsdb instead of settings, to not trigger further events
|
||||
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) {
|
||||
@@ -430,7 +457,7 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it ('does not list users who have no access', function (done) {
|
||||
it ('always lists admins', function (done) {
|
||||
appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [] } }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
@@ -450,7 +477,9 @@ describe('Ldap', function () {
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(0);
|
||||
expect(entries.length).to.equal(1);
|
||||
expect(entries[0].username).to.equal(USER_0.username.toLowerCase());
|
||||
expect(entries[0].memberof.length).to.equal(2);
|
||||
|
||||
appdb.update(APP_0.id, { accessRestriction: null }, done);
|
||||
});
|
||||
@@ -617,13 +646,50 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with pagination', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: 'objectclass=group',
|
||||
paged: true
|
||||
};
|
||||
|
||||
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
|
||||
// ensure order for testability
|
||||
entries.sort(function (a, b) { return a.username < b.username; });
|
||||
|
||||
expect(entries[0].cn).to.equal('users');
|
||||
expect(entries[0].memberuid.length).to.equal(3);
|
||||
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
|
||||
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
|
||||
expect(entries[0].memberuid[2]).to.equal(USER_2.id);
|
||||
expect(entries[1].cn).to.equal('admins');
|
||||
// if only one entry, the array becomes a string :-/
|
||||
expect(entries[1].memberuid).to.equal(USER_0.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function ldapSearch(dn, filter, callback) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: filter
|
||||
filter: filter,
|
||||
paged: true
|
||||
};
|
||||
|
||||
client.search(dn, opts, function (error, result) {
|
||||
@@ -661,7 +727,7 @@ describe('Ldap', function () {
|
||||
});
|
||||
|
||||
it('cannot get alias as a mailbox', function (done) {
|
||||
ldapSearch('cn=' + USER_0_ALIAS + ',ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error, entries) {
|
||||
ldapSearch('cn=' + USER_0_ALIAS + ',ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
@@ -687,7 +753,7 @@ describe('Ldap', function () {
|
||||
});
|
||||
|
||||
it('cannot get mailbox as alias', function (done) {
|
||||
ldapSearch('cn=' + USER_0.username + ',ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error, entries) {
|
||||
ldapSearch('cn=' + USER_0.username + ',ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -162,29 +162,6 @@ describe('Settings', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get default update config config', function (done) {
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(updateConfig.prerelease).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set update config', function (done) {
|
||||
settings.setUpdateConfig({ prerelease: true }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get update config', function (done) {
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(updateConfig.prerelease).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set mail config', function (done) {
|
||||
settings.setMailConfig({ enabled: true }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
@@ -66,7 +66,8 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
|
||||
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })),
|
||||
mailer._clearMailQueue
|
||||
mailer._clearMailQueue,
|
||||
mailer.start
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -113,7 +114,7 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not offer prerelease', function (done) {
|
||||
it('offers prerelease', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
@@ -121,39 +122,18 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-pre.0');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('offers prerelease', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
settings.setUpdateConfig({ prerelease: true }, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-pre.0');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,6 +164,7 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
database.initialize,
|
||||
settings.initialize,
|
||||
mailer._clearMailQueue,
|
||||
mailer.start,
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
|
||||
], done);
|
||||
|
||||
+26
-38
@@ -19,7 +19,6 @@ var apps = require('./apps.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js');
|
||||
|
||||
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
|
||||
@@ -114,7 +113,7 @@ function checkAppUpdates(callback) {
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, updateInfo);
|
||||
mailer.appUpdateAvailable(app, false /* subscription */, updateInfo);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
@@ -124,7 +123,7 @@ function checkAppUpdates(callback) {
|
||||
debug(error);
|
||||
} else if (result === constants.AUTOUPDATE_PATTERN_NEVER) {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, updateInfo);
|
||||
mailer.appUpdateAvailable(app, true /* hasSubscription */, updateInfo);
|
||||
}
|
||||
|
||||
iteratorDone();
|
||||
@@ -149,48 +148,37 @@ function checkBoxUpdates(callback) {
|
||||
appstore.getBoxUpdate(function (error, updateInfo) {
|
||||
if (error || !updateInfo) return callback(error);
|
||||
|
||||
settings.getUpdateConfig(function (error, updateConfig) {
|
||||
gBoxUpdateInfo = updateInfo;
|
||||
|
||||
// decide whether to send email
|
||||
var state = loadState();
|
||||
|
||||
if (state.box === gBoxUpdateInfo.version) {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
return callback();
|
||||
}
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var isPrerelease = semver.parse(updateInfo.version).prerelease.length !== 0;
|
||||
|
||||
if (isPrerelease && !updateConfig.prerelease) {
|
||||
debug('Skipping update %s since this box does not want prereleases', updateInfo.version);
|
||||
return callback();
|
||||
function done() {
|
||||
state.box = updateInfo.version;
|
||||
saveState(state);
|
||||
callback();
|
||||
}
|
||||
|
||||
gBoxUpdateInfo = updateInfo;
|
||||
|
||||
// decide whether to send email
|
||||
var state = loadState();
|
||||
|
||||
if (state.box === gBoxUpdateInfo.version) {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
return callback();
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
mailer.boxUpdateAvailable(false /* hasSubscription */, updateInfo.version, updateInfo.changelog);
|
||||
return done();
|
||||
}
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
// only send notifications if update pattern is 'never'
|
||||
settings.getAutoupdatePattern(function (error, result) {
|
||||
if (error) debug(error);
|
||||
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(true /* hasSubscription */, updateInfo.version, updateInfo.changelog);
|
||||
|
||||
function done() {
|
||||
state.box = updateInfo.version;
|
||||
saveState(state);
|
||||
callback();
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
return done();
|
||||
}
|
||||
|
||||
// only send notifications if update pattern is 'never'
|
||||
settings.getAutoupdatePattern(function (error, result) {
|
||||
if (error) debug(error);
|
||||
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+8
-7
@@ -71,14 +71,14 @@ function getByEmail(email, callback) {
|
||||
function getOwner(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// the first created user it the admin
|
||||
// the first created user it the 'owner'
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY createdAt LIMIT 1',
|
||||
[ constants.ADMIN_GROUP_ID ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
[ constants.ADMIN_GROUP_ID ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, postProcess(result[0]));
|
||||
});
|
||||
callback(null, postProcess(result[0]));
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
@@ -116,7 +116,8 @@ function getAllWithGroupIds(callback) {
|
||||
function getAllAdmins(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY username', [ constants.ADMIN_GROUP_ID ], function (error, results) {
|
||||
// the mailer code relies on the first object being the 'owner' (thus the ORDER)
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY createdAt', [ constants.ADMIN_GROUP_ID ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
@@ -366,10 +366,9 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.updateApp = function (id, manifest, portBindings, callback) {
|
||||
Client.prototype.updateApp = function (id, manifest, callback) {
|
||||
var data = {
|
||||
appStoreId: manifest.id + '@' + manifest.version,
|
||||
portBindings: portBindings
|
||||
appStoreId: manifest.id + '@' + manifest.version
|
||||
};
|
||||
|
||||
post('/api/v1/apps/' + id + '/update', data).success(function (data, status) {
|
||||
@@ -698,13 +697,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getNonApprovedApps = function (callback) {
|
||||
get('/api/v1/developer/apps').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data.apps || []);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getApp = function (appId, callback) {
|
||||
var appFound = null;
|
||||
this._installedApps.some(function (app) {
|
||||
|
||||
+25
-2
@@ -288,6 +288,30 @@ h1, h2, h3 {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-info-title {
|
||||
display: inline-block;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
.app-info-meta {
|
||||
margin-left: 10px;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.app-info-icon {
|
||||
float: left;
|
||||
min-height: 64px;
|
||||
max-height: 64px;
|
||||
min-width: 64px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
multiselect.stretch {
|
||||
button {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Appstore view
|
||||
// ----------------------------
|
||||
@@ -690,7 +714,6 @@ h1, h2, h3 {
|
||||
color: $brand-danger;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
@@ -1166,4 +1189,4 @@ footer {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="(appConfigure.app | installError)">Repair</h4>
|
||||
<h4 class="modal-title" ng-hide="(appConfigure.app | installError)">Configure</h4>
|
||||
<h4 class="modal-title" ng-show="(appConfigure.app | installError)">Repair {{ appConfigure.app.fqdn }}</h4>
|
||||
<h4 class="modal-title" ng-hide="(appConfigure.app | installError)">Configure {{ appConfigure.app.fqdn }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
@@ -88,18 +88,26 @@
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="groups" ng-disabled="groups.length <= 1">
|
||||
Only allow the following user groups <span class="label label-danger" ng-show="appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()">Select at least one group</span>
|
||||
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="groups">
|
||||
Only allow the following users and groups <span class="label label-danger" ng-show="appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()">Select at least one user or group</span>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-show="groups.length <= 1" style="margin-left: 20px;">No groups available. <a href="#/users">Create groups</a></div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<span ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<button class="btn btn-default" type="button" ng-disabled="appConfigure.accessRestrictionOption !== 'groups'" ng-click="appConfigureToggleGroup(group);" ng-class="{ 'btn-primary': (appConfigure.accessRestriction.groups && appConfigure.accessRestriction.groups.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
||||
</span>
|
||||
<div class="col-md-5">
|
||||
Users:
|
||||
<multiselect class="input-sm stretch" ng-model="appConfigure.accessRestriction.users" ng-disabled="appConfigure.accessRestrictionOption !== 'groups'" options="user.username for user in users" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
Groups:
|
||||
<multiselect class="input-sm stretch" ng-model="appConfigure.accessRestriction.groups" ng-disabled="appConfigure.accessRestrictionOption !== 'groups'" options="group.name for group in (groups | ignoreAdminGroup)" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<p ng-show="!mailConfig.enabled && appConfigure.app.manifest.addons.email" class="text-danger">
|
||||
@@ -109,23 +117,19 @@
|
||||
<a href="" ng-click="appConfigure.advancedVisible = true" ng-hide="appConfigure.advancedVisible">Advanced settings...</a>
|
||||
<div uib-collapse="!appConfigure.advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="memoryLimit">Maximum Memory Limit: <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
|
||||
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#increasing-the-memory-limit-of-an-app" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
|
||||
<br/>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryLimit" ng-model="appConfigure.memoryLimit" step="134217728" tooltip="hide" ticks="appConfigure.memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.xFrameOptions.$dirty && appConfigure.error.xFrameOptions }">
|
||||
<label class="control-label">Allow embedding from the following site</label>
|
||||
<div class="control-label" ng-show="appConfigure.error.xFrameOptions"><small>Must be empty of a valid URL</small></div>
|
||||
<input type="text" class="form-control" id="appConfigureXFrameOptionsInput" name="xFrameOptions" placeholder="https://example.com" ng-model="appConfigure.xFrameOptions" uib-tooltip="Leave blank to not allow embedding">
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Specify robots.txt file content</label>
|
||||
<textarea ng-model="appConfigure.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="3"></textarea>
|
||||
@@ -179,8 +183,11 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Restore {{ appRestore.app.fqdn }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="appRestore.backups.length === 0">
|
||||
<p class="text-danger">This app has no backups.</p>
|
||||
<div class="modal-body" ng-show="appRestore.busyFetching">
|
||||
<h4 class="text-center"><i class="fa fa-circle-o-notch fa-spin"></i> Fetching backups</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="appRestore.backups.length === 0 && !appRestore.busyFetching">
|
||||
<h4 class="text-danger">This app has no backups.</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="appRestore.backups.length !== 0">
|
||||
<p>Restoring the app will lose all content generated since the backup.</p>
|
||||
@@ -195,7 +202,7 @@
|
||||
</div>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form role="form" name="appRestoreForm" ng-submit="doRestore()" autocomplete="off">
|
||||
<form role="form" name="appRestoreForm" ng-submit="appRestore.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password) }">
|
||||
<label class="control-label" for="appRestorePasswordInput">Provide your password to confirm this action</label>
|
||||
<div class="control-label" ng-show="(appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password)">
|
||||
@@ -211,22 +218,30 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doRestore()" ng-disabled="appRestoreForm.$invalid || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Restore</button>
|
||||
<button type="button" class="btn btn-success" ng-click="appRestore.submit()" ng-show="appRestore.backups.length !== 0" ng-disabled="appRestoreForm.$invalid || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal post install message app -->
|
||||
<div class="modal fade" id="appPostInstallModal" tabindex="-1" role="dialog">
|
||||
<!-- Modal information of app -->
|
||||
<div class="modal fade" id="appInfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Installation notes for {{ appPostInstall.app.manifest.title }}</h4>
|
||||
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">{{ appInfo.app.manifest.title }}</h5>
|
||||
<br/>
|
||||
<span class="app-info-meta">Package version <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">{{ appInfo.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span class="app-info-meta" ng-show="appInfo.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">Documentation</a> </span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="app-postinstall-message">
|
||||
<div ng-bind-html="appPostInstall.message | postInstallMessage:appPostInstall.app | markdown2html"></div>
|
||||
<div class="app-postinstall-message" ng-hide="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
|
||||
This package has no special usage information.
|
||||
</div>
|
||||
<div class="app-postinstall-message" ng-show="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
|
||||
<div ng-bind-html="appInfo.message | postInstallMessage:appInfo.app | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -295,34 +310,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
|
||||
<pre>{{ appUpdate.manifest.changelog }}</pre>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
|
||||
<div ng-repeat="(env, info) in appUpdate.portBindingsInfo" ng-class="{ 'newPort': info.isNew }">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appUpdate.portBindingsEnabled[env]"> <span ng-show="info.isNew">New - </span> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
|
||||
<input type="number" class="form-control" ng-model="appUpdate.portBindings[env]" ng-disabled="!appUpdate.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<div ng-repeat="(env, port) in appUpdate.obsoletePortBindings" class="obsoletePort">
|
||||
<ng-form name="obsoletePortInfo_form">
|
||||
<div class="form-group">
|
||||
Obsolete -
|
||||
<label class="control-label">{{ env }}</label>
|
||||
<input type="number" class="form-control" ng-model="port" disabled>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
<div ng-bind-html="appUpdate.manifest.changelog | markdown2html"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate(appUpdateForm)" ng-disabled="appUpdateForm.$invalid || appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -401,7 +393,7 @@
|
||||
<div class="grid-item-bottom-mobile" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-left">
|
||||
<a href="" ng-click="showRestore(app)" ng-show="backupConfig.provider !== 'noop'">
|
||||
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'">
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
@@ -424,7 +416,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="" ng-click="showRestore(app)" ng-show="backupConfig.provider !== 'noop'" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
@@ -435,8 +427,8 @@
|
||||
<a href="" ng-click="showConfigure(app)" title="Repair App"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="hasPostInstallMessage(app)">
|
||||
<a href="" ng-click="showPostInstall(app)" title="Information"><i class="fa fa-info-circle scale"></i></a>
|
||||
<div>
|
||||
<a href="" ng-click="showInformation(app)" title="Information"><i class="fa fa-info-circle scale"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+77
-150
@@ -63,6 +63,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
|
||||
$scope.appRestore = {
|
||||
busy: false,
|
||||
busyFetching: false,
|
||||
error: {},
|
||||
app: {},
|
||||
password: '',
|
||||
@@ -71,10 +72,51 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
|
||||
selectBackup: function (backup) {
|
||||
$scope.appRestore.selectedBackup = backup;
|
||||
},
|
||||
|
||||
show: function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appRestore.app = app;
|
||||
$scope.appRestore.busyFetching = true;
|
||||
|
||||
$('#appRestoreModal').modal('show');
|
||||
|
||||
Client.getAppBackups(app.id, function (error, backups) {
|
||||
if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$scope.appRestore.backups = backups;
|
||||
if (backups.length) $scope.appRestore.selectedBackup = backups[0]; // pre-select first backup
|
||||
$scope.appRestore.busyFetching = false;
|
||||
}
|
||||
});
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appRestore.busy = true;
|
||||
$scope.appRestore.error.password = null;
|
||||
|
||||
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.selectedBackup.id, $scope.appRestore.password, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appRestore.password = '';
|
||||
$scope.appRestore.error.password = true;
|
||||
$scope.appRestoreForm.password.$setPristine();
|
||||
$('#appRestorePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#appRestoreModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.appRestore.busy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appPostInstall = {
|
||||
$scope.appInfo = {
|
||||
app: {},
|
||||
message: ''
|
||||
};
|
||||
@@ -97,7 +139,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$('#appConfigureModal').modal('hide');
|
||||
$('#appRestoreModal').modal('hide');
|
||||
$('#appUpdateModal').modal('hide');
|
||||
$('#appPostInstallModal').modal('hide');
|
||||
$('#appInfoModal').modal('hide');
|
||||
$('#appUninstallModal').modal('hide');
|
||||
|
||||
// reset configure dialog
|
||||
@@ -136,10 +178,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appUpdate.error = {};
|
||||
$scope.appUpdate.app = {};
|
||||
$scope.appUpdate.manifest = {};
|
||||
$scope.appUpdate.portBindings = {};
|
||||
|
||||
$scope.appUpdateForm.$setPristine();
|
||||
$scope.appUpdateForm.$setUntouched();
|
||||
|
||||
// reset restore dialog
|
||||
$scope.appRestore.error = {};
|
||||
@@ -180,14 +218,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.appConfigureToggleGroup = function (group) {
|
||||
var groups = $scope.appConfigure.accessRestriction.groups;
|
||||
var pos = groups.indexOf(group.id);
|
||||
|
||||
if (pos === -1) groups.push(group.id);
|
||||
else groups.splice(pos, 1);
|
||||
};
|
||||
|
||||
$scope.useAltDomain = function (use) {
|
||||
$scope.appConfigure.usingAltDomain = use;
|
||||
|
||||
@@ -206,27 +236,36 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.location = app.altDomain || app.location;
|
||||
$scope.appConfigure.usingAltDomain = !!app.altDomain;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
|
||||
$scope. Option = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// create ticks starting from manifest memory limit
|
||||
$scope.appConfigure.memoryTicks = [
|
||||
256 * 1024 * 1024,
|
||||
512 * 1024 * 1024,
|
||||
1024 * 1024 * 1024,
|
||||
2048 * 1024 * 1024,
|
||||
4096 * 1024 * 1024
|
||||
].filter(function (t) { return t >= (app.manifest.memoryLimit || 0); });
|
||||
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
||||
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
||||
$scope.appConfigure.memoryTicks = [ ];
|
||||
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
|
||||
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
||||
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
}
|
||||
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
if (app.accessRestriction) {
|
||||
var userSet = { };
|
||||
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); });
|
||||
|
||||
var groupSet = { };
|
||||
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
@@ -256,11 +295,18 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
|
||||
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: $scope.appConfigure.accessRestrictionOption === 'groups' ? $scope.appConfigure.accessRestriction : null,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
@@ -303,13 +349,13 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showPostInstall = function (app) {
|
||||
$scope.showInformation = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appPostInstall.app = app;
|
||||
$scope.appPostInstall.message = app.manifest.postInstallMessage;
|
||||
$scope.appInfo.app = app;
|
||||
$scope.appInfo.message = app.manifest.postInstallMessage;
|
||||
|
||||
$('#appPostInstallModal').modal('show');
|
||||
$('#appInfoModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
};
|
||||
@@ -324,48 +370,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
return false; // prevent propagation and default
|
||||
};
|
||||
|
||||
$scope.showRestore = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appRestore.app = app;
|
||||
$scope.appRestore.busy = true;
|
||||
|
||||
$('#appRestoreModal').modal('show');
|
||||
|
||||
Client.getAppBackups(app.id, function (error, backups) {
|
||||
if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$scope.appRestore.backups = backups;
|
||||
if (backups.length) $scope.appRestore.selectedBackup = backups[0]; // pre-select first backup
|
||||
$scope.appRestore.busy = false;
|
||||
}
|
||||
});
|
||||
|
||||
return false; // prevent propagation and default
|
||||
};
|
||||
|
||||
$scope.doRestore = function () {
|
||||
$scope.appRestore.busy = true;
|
||||
$scope.appRestore.error.password = null;
|
||||
|
||||
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.selectedBackup.id, $scope.appRestore.password, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appRestore.password = '';
|
||||
$scope.appRestore.error.password = true;
|
||||
$scope.appRestoreForm.password.$setPristine();
|
||||
$('#appRestorePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#appRestoreModal').modal('hide');
|
||||
$scope.reset();
|
||||
}
|
||||
|
||||
$scope.appRestore.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showUninstall = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
@@ -408,90 +412,17 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appUpdate.app = app;
|
||||
$scope.appUpdate.manifest = angular.copy(updateManifest);
|
||||
|
||||
// ensure we always operate on objects here
|
||||
app.portBindings = app.portBindings || {};
|
||||
app.manifest.tcpPorts = app.manifest.tcpPorts || {};
|
||||
updateManifest.tcpPorts = updateManifest.tcpPorts || {};
|
||||
|
||||
// Activate below two lines for testing the UI
|
||||
// updateManifest.tcpPorts['TEST_HTTP'] = { defaultValue: 1337, description: 'HTTP server'};
|
||||
// app.manifest.tcpPorts['TEST_FOOBAR'] = { defaultValue: 1338, description: 'FOOBAR server'};
|
||||
// app.portBindings['TEST_SSH'] = 1339;
|
||||
|
||||
var portBindingsInfo = {}; // Portbinding map only for information
|
||||
var portBindings = {}; // This is the actual model holding the env:port pair
|
||||
var portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
var obsoletePortBindings = {}; // Info map for obsolete port bindings, this is for display use only and thus not in the model
|
||||
var portsChanged = false;
|
||||
var env;
|
||||
|
||||
// detect new portbindings and copy all from manifest.tcpPorts
|
||||
for (env in updateManifest.tcpPorts) {
|
||||
portBindingsInfo[env] = updateManifest.tcpPorts[env];
|
||||
if (!app.manifest.tcpPorts[env]) {
|
||||
portBindingsInfo[env].isNew = true;
|
||||
portBindingsEnabled[env] = true;
|
||||
|
||||
// use default integer port value in model
|
||||
portBindings[env] = updateManifest.tcpPorts[env].defaultValue || 0;
|
||||
|
||||
portsChanged = true;
|
||||
} else {
|
||||
// detect if the port binding was enabled
|
||||
if (app.portBindings[env]) {
|
||||
portBindings[env] = app.portBindings[env];
|
||||
portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
portBindings[env] = updateManifest.tcpPorts[env].defaultValue || 0;
|
||||
portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect obsolete portbindings (mappings in app.portBindings, but not anymore in updateManifest.tcpPorts)
|
||||
for (env in app.manifest.tcpPorts) {
|
||||
// only list the port if it is not in the new manifest and was enabled previously
|
||||
if (!updateManifest.tcpPorts[env] && app.portBindings[env]) {
|
||||
obsoletePortBindings[env] = app.portBindings[env];
|
||||
portsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// now inject the maps into the $scope, we only show those if ports have changed
|
||||
$scope.appUpdate.portBindings = portBindings; // always inject the model, so it gets used in the actual update call
|
||||
$scope.appUpdate.portBindingsEnabled = portBindingsEnabled; // always inject the model, so it gets used in the actual update call
|
||||
|
||||
if (portsChanged) {
|
||||
$scope.appUpdate.portBindingsInfo = portBindingsInfo;
|
||||
$scope.appUpdate.obsoletePortBindings = obsoletePortBindings;
|
||||
} else {
|
||||
$scope.appUpdate.portBindingsInfo = {};
|
||||
$scope.appUpdate.obsoletePortBindings = {};
|
||||
}
|
||||
|
||||
$('#appUpdateModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doUpdate = function (form) {
|
||||
$scope.doUpdate = function () {
|
||||
$scope.appUpdate.busy = true;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appUpdate.portBindings) {
|
||||
if ($scope.appUpdate.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appUpdate.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
Client.updateApp($scope.appUpdate.app.id, $scope.appUpdate.manifest, finalPortBindings, function (error) {
|
||||
Client.updateApp($scope.appUpdate.app.id, $scope.appUpdate.manifest, function (error) {
|
||||
if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$scope.appUpdate.app = {};
|
||||
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
|
||||
$('#appUpdateModal').modal('hide');
|
||||
}
|
||||
|
||||
@@ -512,10 +443,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
$scope.hasPostInstallMessage = function (app) {
|
||||
return app.manifest && app.manifest.postInstallMessage;
|
||||
};
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getUsers(function (error, users) {
|
||||
if (error) {
|
||||
|
||||
@@ -72,18 +72,26 @@
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups" ng-disabled="groups.length <= 1">
|
||||
Only allow the following user groups <span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">Select at least one group</span>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups">
|
||||
Only allow the following users and groups <span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">Select at least one user or group</span>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-show="groups.length <= 1" style="margin-left: 20px;">No groups available. <a href="" ng-click="showView('/users')">Create groups</a></div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<span ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<button class="btn btn-default" type="button" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" ng-click="appInstall.toggleGroup(group);" ng-class="{ 'btn-primary': (appInstall.accessRestriction.groups && appInstall.accessRestriction.groups.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
||||
</span>
|
||||
<div class="col-md-5">
|
||||
Users:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="user.username for user in users" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
Groups:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in (groups | ignoreAdminGroup)" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<p ng-show="!mailConfig.enabled && appInstall.app.manifest.addons.email" class="text-danger">
|
||||
|
||||
@@ -53,14 +53,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
|
||||
toggleGroup: function (group) {
|
||||
var groups = $scope.appInstall.accessRestriction.groups;
|
||||
var pos = groups.indexOf(group.id);
|
||||
|
||||
if (pos === -1) groups.push(group.id);
|
||||
else groups.splice(pos, 1);
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
@@ -144,13 +136,17 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
}
|
||||
}
|
||||
|
||||
// translate to accessRestriction object
|
||||
var accessRestriction = $scope.appInstall.accessRestrictionOption === 'groups' ? $scope.appInstall.accessRestriction : null;
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appInstall.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appInstall.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appInstall.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: accessRestriction,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
|
||||
@@ -353,18 +349,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
|
||||
return callback(null, apps);
|
||||
|
||||
// Client.getNonApprovedApps(function (error, result) {
|
||||
// if (error) return callback(error);
|
||||
|
||||
// // add testing tag to the manifest for UI and search reasons
|
||||
// result.forEach(function (app) {
|
||||
// if (!app.manifest.tags) app.manifest.tags = [];
|
||||
// app.manifest.tags.push('testing');
|
||||
// });
|
||||
|
||||
// callback(null, apps.concat(result));
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="dnsCredentials.busy" required>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
@@ -90,7 +90,7 @@
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'manual'">
|
||||
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
Setup an <i>A</i> record for <b>{{ config.adminLocation }}.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
<div class="logs-controls">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<uib-tabset active="active">
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
<uib-tab index="0" heading="Logs" select="showLogs()"></uib-tab>
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
</uib-tabset>
|
||||
<select class="form-control pull-right inline" ng-options="log.name for log in logs track by log.value" ng-model="selected"></select>
|
||||
|
||||
|
||||
@@ -27,18 +27,32 @@
|
||||
</div>
|
||||
|
||||
<!-- Test email sent -->
|
||||
<div class="modal fade" id="testEmailSent" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Test Email Sent</h4>
|
||||
<h4 class="modal-title">Send test email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A test email was sent to {{ user.email }}. Please check the inbox of this account and verify the email was delivered.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Ok</button>
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">Email to</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="Email address" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,9 +79,9 @@
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#mail_settings">Mail server settings for email clients</a>
|
||||
<div id="mail_settings" class="panel-collapse collapse">
|
||||
<br/>
|
||||
<p><b>Incoming Mail (IMAP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 993 (TLS)</p>
|
||||
<p><b>Outgoing Mail (SMTP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 587 (STARTTLS)</p>
|
||||
<p><b>ManageSieve</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 4190 (TLS)</p>
|
||||
<p><b>Incoming Mail (IMAP)</b><br/>Server: <span ng-click-select>{{config.mailFqdn}}</span><br/>Port: 993 (TLS)</p>
|
||||
<p><b>Outgoing Mail (SMTP)</b><br/>Server: <span ng-click-select>{{config.mailFqdn}}</span><br/>Port: 587 (STARTTLS)</p>
|
||||
<p><b>ManageSieve</b><br/>Server: <span ng-click-select>{{config.mailFqdn}}</span><br/>Port: 4190 (TLS)</p>
|
||||
<p>All the servers require your Cloudron credentials for authentication.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +210,7 @@
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
@@ -226,7 +240,7 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
Outbound SMTP
|
||||
</a>
|
||||
@@ -240,10 +254,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="rbl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">
|
||||
IP Address Blacklist Check
|
||||
</a>
|
||||
@@ -260,9 +274,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-left" ng-click="sendTestEmail()">Send Test Email</button>
|
||||
<button class="btn btn-primary pull-left" ng-click="testEmail.show()">Send Test Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,13 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.dnsConfig = {};
|
||||
$scope.relay = {};
|
||||
$scope.rbl = null;
|
||||
$scope.expectedDnsRecords = {};
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
dkim: { },
|
||||
spf: { },
|
||||
dmarc: { },
|
||||
ptr: { }
|
||||
};
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
@@ -158,12 +164,42 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sendTestEmail = function () {
|
||||
Client.sentTestMail($scope.user.email, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.testEmail = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
$('#testEmailSent').modal('show');
|
||||
});
|
||||
mailTo: '',
|
||||
|
||||
clearForm: function () {
|
||||
$scope.testEmail.mailTo = '';
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
$scope.testEmail.mailTo = $scope.user.email;
|
||||
|
||||
$('#testEmailModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = true;
|
||||
|
||||
Client.sentTestMail($scope.testEmail.mailTo, function (error) {
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.testEmail.error.generic = error.message;
|
||||
console.error(error);
|
||||
$('#inputTestMailTo').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#testEmailModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getMailConfig() {
|
||||
@@ -217,12 +253,13 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.expectedDnsRecords = result.dns;
|
||||
$scope.relay = result.relay;
|
||||
$scope.rbl = result.rbl;
|
||||
|
||||
// open the record details if they are not correct
|
||||
for (var type in $scope.expectedDnsRecords) {
|
||||
$scope.expectedDnsRecords[type] = result.dns[type];
|
||||
|
||||
if (!$scope.expectedDnsRecords[type].status) {
|
||||
$('#collapse_dns_' + type).collapse('show');
|
||||
}
|
||||
@@ -285,5 +322,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.email.refresh();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['testEmailModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -142,10 +142,18 @@
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
|
||||
<p class="has-error" ng-show="configureBackup.provider === 'filesystem'">
|
||||
Please ensure that the backup directory is an external disk
|
||||
Please ensure that the backup directory is an external ext4 disk
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks" id="inputConfigureUseHardlinks">
|
||||
Use hardlinks
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
|
||||
@@ -327,6 +327,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
backupFolder: '',
|
||||
retentionSecs: 7 * 24 * 60 * 60,
|
||||
acceptSelfSignedCerts: false,
|
||||
useHardlinks: true,
|
||||
format: 'tgz',
|
||||
|
||||
clearForm: function () {
|
||||
@@ -340,6 +341,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.configureBackup.retentionSecs = 7 * 24 * 60 * 60;
|
||||
$scope.configureBackup.format = 'tgz';
|
||||
$scope.configureBackup.acceptSelfSignedCerts = false;
|
||||
$scope.configureBackup.useHardlinks = true;
|
||||
},
|
||||
|
||||
show: function () {
|
||||
@@ -358,6 +360,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.configureBackup.retentionSecs = $scope.backupConfig.retentionSecs;
|
||||
$scope.configureBackup.format = $scope.backupConfig.format;
|
||||
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
|
||||
$('#configureBackupModal').modal('show');
|
||||
},
|
||||
@@ -397,6 +400,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
}
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
@@ -511,8 +515,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
$scope.backupConfig = backupConfig;
|
||||
|
||||
// Check if a proper storage backend is configured
|
||||
if (backupConfig.provider === 'filesystem') {
|
||||
// Check if a proper storage backend is configured. TODO: this check fails if /var/backups is actually external
|
||||
if (backupConfig.provider === 'filesystem' && backupConfig.backupFolder === '/var/backups') {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/settings';
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
|
||||
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error;
|
||||
$scope.feedback.error = error.message;
|
||||
} else {
|
||||
$scope.feedback.success = true;
|
||||
resetFeedback();
|
||||
|
||||
Reference in New Issue
Block a user