Compare commits

..

67 Commits

Author SHA1 Message Date
Girish Ramakrishnan ca60d4c8b8 1.8.4 changes 2017-12-07 20:17:35 +05:30
Girish Ramakrishnan 2ce00ca0d7 Fix display of DNS records when not using cloudron-smtp
Fixes #492
2017-12-07 20:13:28 +05:30
Girish Ramakrishnan a57db15b63 Bump mail container for internal email relay fix 2017-12-07 20:12:34 +05:30
Johannes Zellner f14a8b0ab0 add 1.8.3 changes 2017-11-23 02:48:16 +01:00
Girish Ramakrishnan 5f207716e5 1.8.2 changes 2017-11-23 02:48:16 +01:00
Johannes Zellner 010d48035b Setup domain record in settingsdb during dns setup 2017-11-23 02:48:12 +01:00
Girish Ramakrishnan 7e6a83df84 Fix migration callback 2017-11-18 02:11:00 -08:00
Girish Ramakrishnan ec4910a45e Fix restore 2017-11-17 22:35:56 -08:00
Girish Ramakrishnan 6558c78094 change the json blobs to text 2017-11-17 15:52:40 -08:00
Girish Ramakrishnan 5df92d1903 remove dead code 2017-11-17 15:18:06 -08:00
Girish Ramakrishnan 05affa7d26 remove dead code 2017-11-17 15:17:50 -08:00
Girish Ramakrishnan 46c6c5a5a8 remove double .js 2017-11-17 14:50:53 -08:00
Girish Ramakrishnan 75da751c72 1.8.1 changes 2017-11-17 14:50:53 -08:00
Johannes Zellner b84f60671e Also fix the restoreConfigJson migration down script 2017-11-17 23:45:22 +01:00
Johannes Zellner 8dcb06cb02 Fix db migration down step for newConfigJson change 2017-11-17 23:41:22 +01:00
Girish Ramakrishnan 83bf739081 Update the license 2017-11-17 10:46:12 -08:00
Girish Ramakrishnan 48a52fae2e LE agreement URL has changed 2017-11-17 10:35:58 -08:00
Girish Ramakrishnan 0ddbda6068 Fix crash 2017-11-16 15:11:12 -08:00
Girish Ramakrishnan 360fa058ea store format information for restoring
fixes #483
2017-11-16 15:01:27 -08:00
Johannes Zellner 489d2022e6 Do not underline errored links 2017-11-16 23:18:50 +01:00
Girish Ramakrishnan f762d0c0a1 newConfig -> updateConfig 2017-11-16 12:36:07 -08:00
Girish Ramakrishnan 98cad0678d Handle json parse errors with new body-parser module 2017-11-16 11:47:17 -08:00
Girish Ramakrishnan 92acb2954f Rename restoreConfig to manifest in backup table
Only the manifest needs to be preserved in the backup table
2017-11-16 11:25:40 -08:00
Girish Ramakrishnan 00a6e4c982 Show doc url in info dialog
Fixes #486
2017-11-16 10:05:49 -08:00
Girish Ramakrishnan bf9eb4bd87 Switch the default to logs to show some useful information 2017-11-16 10:05:49 -08:00
Girish Ramakrishnan 2f4940acbd update modules 2017-11-16 09:34:00 -08:00
Girish Ramakrishnan 9f7ca552a6 handle various appstore errors 2017-11-16 00:23:34 -08:00
Girish Ramakrishnan 4272d5be8a Send feedback via API
Fixes #484
2017-11-15 23:31:13 -08:00
Girish Ramakrishnan 1babfb6e87 Allow admins to access all apps
Fixes #420
2017-11-15 19:24:11 -08:00
Girish Ramakrishnan 5663cf45f8 remove redundant reset 2017-11-15 19:08:38 -08:00
Girish Ramakrishnan d8cb2d1d25 test: reset is already part of setup 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 174a60bb07 fix linter warnings 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 3d7094bf28 Handle error in uploadFile 2017-11-15 18:45:23 -08:00
Girish Ramakrishnan 4d6616930a Fix failing test 2017-11-15 18:41:37 -08:00
Girish Ramakrishnan 24875ba292 Handle all errors and set focus correctly
Fixes #485
2017-11-14 18:26:42 -08:00
Johannes Zellner c58b2677b6 Fixup config tests and do not allow saving random values to the config file
Those will eventually be overwritten by start.sh anyways, we cannot rely
on those
2017-11-15 02:41:40 +01:00
Johannes Zellner 25146e1134 Allow tests to work without a cloudron.conf on disk 2017-11-15 02:40:50 +01:00
Johannes Zellner c0c35964fe Fix backups tests 2017-11-15 02:29:58 +01:00
Johannes Zellner 0bf9ab0a2b No need to put static database config in cloudron.conf 2017-11-15 02:29:36 +01:00
Johannes Zellner 6d86f4cbda Ensure we only save relevant config values 2017-11-15 02:29:07 +01:00
Girish Ramakrishnan d2741bbeb9 Allow mailTo to be configurable
Part of #485
2017-11-14 16:24:34 -08:00
Girish Ramakrishnan 690d02a353 Always show the DNS records in the UI 2017-11-14 15:13:56 -08:00
Johannes Zellner c629db9597 Remove preinstall app bundle support 2017-11-14 23:09:17 +01:00
Girish Ramakrishnan 67fcf85abb Allow restore if already restoring 2017-11-13 18:43:36 -08:00
Girish Ramakrishnan 527eace8f8 Fix j2xml usage 2017-11-13 11:10:42 -08:00
Girish Ramakrishnan e65230b833 update many dev modules 2017-11-13 10:57:36 -08:00
Girish Ramakrishnan 3e8334040b Update many node modules
also, use rimraf instead of del
2017-11-13 10:57:32 -08:00
Girish Ramakrishnan 2bcd3a8e4d Add a hack to stretch the multi-select box a bit 2017-11-12 02:50:28 -08:00
Girish Ramakrishnan e75b85fc3a Bump postgresql container to workaround shm issues
reconfiguring the postgresql configuring seems to fix some shm
issues on docker upgrade
2017-11-11 20:52:34 -08:00
Girish Ramakrishnan c4362d3339 Fix failing ldap test 2017-11-11 17:33:27 -08:00
Girish Ramakrishnan 85e492a632 Fix detection of container id from IP
https://docs.docker.com/engine/api/v1.32/#tag/Network

"Note that it uses a different, smaller representation of a network
than inspecting a single network. For example, the list of containers
attached to the network is not propagated in API versions 1.28 and up."

Verified using:

curl --unix-socket /var/run/docker.sock http::/networks/cloudron
2017-11-11 16:55:43 -08:00
Girish Ramakrishnan b8d4b67043 update aws-sdk and dockerode 2017-11-11 16:38:40 -08:00
Girish Ramakrishnan ffacd31259 bump the node version 2017-11-11 16:25:42 -08:00
Johannes Zellner 19f6da88da Do not disable access control elements if no group was created
There are still users to be selected
2017-11-12 00:09:05 +01:00
Girish Ramakrishnan c0faae4e27 Add more changes for 1.8.0 2017-11-11 11:14:42 -08:00
Girish Ramakrishnan a19c566eea Always show info box that displays app version
Fixes #478
2017-11-11 11:09:59 -08:00
Girish Ramakrishnan 3ec806452c Update node to 6.11.5 2017-11-10 19:25:08 -08:00
Girish Ramakrishnan 0c73cd5219 Update docker to 17.09 2017-11-10 18:49:28 -08:00
Girish Ramakrishnan 9b6bf719ff 1.7.8 changes 2017-11-09 09:40:26 -08:00
Girish Ramakrishnan 25431d3cc4 Fix the spacing 2017-11-09 09:29:42 -08:00
Girish Ramakrishnan e0805df3b1 Only show backup warning if using default location 2017-11-09 09:09:39 -08:00
Girish Ramakrishnan 8392fec570 Remove the bold 2017-11-08 20:57:40 -08:00
Girish Ramakrishnan 1c173ca83f Add UI to select users for access restriction 2017-11-08 20:54:38 -08:00
Girish Ramakrishnan 05a67db761 backup must be stored in ext4
Other file systems like FAT/CIFS can error with cryptic error messages
when saving filenames with special characters such as ':'
2017-11-08 12:26:25 -08:00
Girish Ramakrishnan bb24d5cf9e Order eventlog entries by time 2017-11-08 09:14:55 -08:00
Girish Ramakrishnan 8d2fbe931f Bump max limit to two times ram
part of #466
2017-11-07 10:07:05 -08:00
Girish Ramakrishnan 0a8adaac9f filter out empty usernames from groups
Fixes #472
2017-11-06 11:09:40 -08:00
56 changed files with 1762 additions and 1573 deletions
+36
View File
@@ -1102,3 +1102,39 @@
* 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)
[1.8.2]
* 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)
[1.8.3]
* Ensure domain database record exists
[1.8.4]
* Fix issue where internal email was not delivered when email relay is enabled
* Fix display of DNS records when email relay is enabled
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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);
});
};
+3 -3
View File
@@ -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));
+976 -1016
View File
File diff suppressed because it is too large Load Diff
+39 -38
View File
@@ -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",
+3 -11
View File
@@ -263,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}"
+2 -2
View 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
View File
@@ -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}"
-4
View File
@@ -22,7 +22,6 @@ arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_provider=""
arg_app_bundle=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
@@ -59,9 +58,6 @@ while true; do
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"
+1 -9
View File
@@ -272,15 +272,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"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
+14 -10
View File
@@ -60,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(',');
@@ -76,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');
@@ -193,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) {
@@ -322,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') {
@@ -376,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) {
+54 -33
View File
@@ -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'),
@@ -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,7 +654,7 @@ function update(appId, data, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
newConfig.manifest = manifest;
updateConfig.manifest = manifest;
if ('icon' in data) {
if (data.icon) {
@@ -657,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));
@@ -754,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)
};
@@ -808,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));
@@ -838,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'
};
@@ -1021,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) {
@@ -1091,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);
});
@@ -1192,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);
+25
View File
@@ -13,6 +13,8 @@ exports = module.exports = {
getAccount: getAccount,
sendFeedback: sendFeedback,
AppstoreError: AppstoreError
};
@@ -267,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);
});
});
}
+12 -12
View File
@@ -389,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
@@ -429,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),
@@ -437,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);
}
},
@@ -457,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),
@@ -575,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);
@@ -599,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
@@ -609,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);
},
@@ -621,7 +621,7 @@ function update(app, callback) {
function (next) {
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.newConfig.manifest.tcpPorts || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newPorts[portName]) return callback(); // port still in use
@@ -639,7 +639,7 @@ function update(app, callback) {
},
// 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),
@@ -661,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
View File
@@ -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));
+19 -47
View File
@@ -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);
});
}
@@ -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) {
+1 -1
View File
@@ -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,
+12 -36
View File
@@ -52,6 +52,7 @@ var appdb = require('./appdb.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
settingsdb = require('./settingsdb.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
@@ -126,7 +127,6 @@ function initialize(callback) {
async.series([
certificates.initialize,
settings.initialize,
installAppBundle,
configureDefaultServer,
onDomainConfigured
], function (error) {
@@ -184,12 +184,18 @@ function dnsSetup(dnsConfig, domain, zoneName, callback) {
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setZoneName(zoneName);
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
// upsert the temporary domain record in settings db
// This can be removed after this release
settingsdb.set('domain', JSON.stringify({ fqdn: domain, zoneName: zoneName }), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
callback();
});
});
}
@@ -741,36 +747,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;
+45 -28
View File
@@ -48,12 +48,17 @@ exports = module.exports = {
};
var assert = require('assert'),
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 = { };
@@ -65,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) {
@@ -79,46 +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();
+3 -9
View File
@@ -369,19 +369,13 @@ function getContainerIdByIp(ip, callback) {
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;
}
+1 -1
View File
@@ -77,7 +77,7 @@ function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ?';
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));
+2 -2
View File
@@ -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.39.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.1' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
}
};
+2 -2
View File
@@ -479,13 +479,13 @@ function start(callback) {
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();
-14
View File
@@ -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
View File
@@ -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; });
+1 -32
View File
@@ -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'),
@@ -575,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');
-1
View File
@@ -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
View File
@@ -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'));
+27 -22
View File
@@ -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) {
+2 -2
View File
@@ -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'),
@@ -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)
+36 -36
View File
@@ -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 })
+1 -1
View File
@@ -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();
});
});
+4 -2
View File
@@ -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();
});
});
+3 -3
View File
@@ -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' } }));
+10 -7
View File
@@ -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'
};
+9 -10
View File
@@ -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://my.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();
});
+10 -10
View File
@@ -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();
});
+8 -19
View File
@@ -12,7 +12,6 @@ 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'),
@@ -37,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();
@@ -59,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');
@@ -70,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);
@@ -85,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);
});
});
@@ -100,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);
});
});
@@ -110,19 +111,11 @@ 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 owner account email', function (done) {
var subscription = {
id: 'caas',
created: 0,
canceled_at: 0,
status: 'active',
plan: { id: 'caas' }
};
updatechecker._setUpdateInfo({ box: null, apps: { 'appid': { manifest: { version: '1.2.5', changelog: 'noop\nreally' } } } });
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) {
@@ -131,11 +124,7 @@ describe('digest', function () {
digest.maybeSend(function (error) {
if (error) return done(error);
checkMails(1, [ 'user0@email.com, username0@localhost' ], function (error) {
if (error) return done(error);
done();
});
checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done);
});
});
});
+25 -3
View File
@@ -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();
});
});
+27 -27
View File
@@ -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();
});
@@ -459,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);
@@ -479,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);
});
@@ -727,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();
});
@@ -753,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();
});
+25 -2
View File
@@ -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%;
}
}
+32 -20
View File
@@ -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 <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>
<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>
@@ -220,16 +224,24 @@
</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">
@@ -415,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>
+28 -22
View File
@@ -116,7 +116,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
};
$scope.appPostInstall = {
$scope.appInfo = {
app: {},
message: ''
};
@@ -139,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
@@ -218,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;
@@ -244,8 +236,7 @@ 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']);
@@ -255,7 +246,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
// 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 = [ ];
for (var i = 256; i <= ($scope.config.memory*2/1024/1024); i *= 2) {
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) {
@@ -263,6 +255,17 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
$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) {
@@ -292,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',
@@ -339,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
};
@@ -433,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) {
+14 -6
View File
@@ -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: &nbsp;
<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: &nbsp;
<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">
+7 -11
View File
@@ -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')
+1 -1
View File
@@ -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>
+32 -15
View File
@@ -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>
@@ -181,12 +195,13 @@
</div>
</div>
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
<h3>DNS Records</h3>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
<div class="row">
<div class="col-md-12">
Set the following DNS records to guarantee email delivery:
@@ -196,7 +211,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> &nbsp;
<i ng-hide="email.refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<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>
@@ -211,8 +226,10 @@
</div>
</div>
</div>
</div>
</div>
</div>
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
@@ -226,13 +243,13 @@
<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> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
<i ng-hide="email.refreshBusy" ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
Outbound SMTP
</a>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</p>
<div id="collapse_dns_port" class="panel-collapse collapse">
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ relay.value }} </b> </p>
</div>
@@ -240,10 +257,10 @@
</div>
</div>
<div class="row" ng-show="rbl">
<div class="row" ng-show="currentRelay.provider === 'cloudron-smtp'">
<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> &nbsp;
<i ng-hide="email.refreshBusy" ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">
IP Address Blacklist Check
</a>
@@ -262,7 +279,7 @@
<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>
+83 -13
View File
@@ -7,9 +7,16 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.dnsConfig = {};
$scope.currentRelay = {};
$scope.relay = {};
$scope.rbl = null;
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecords = {
mx: { },
dkim: { },
spf: { },
dmarc: { },
ptr: { }
};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
{ name: 'DKIM', value: 'dkim' },
@@ -81,6 +88,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
refresh: function () {
$scope.email.refreshBusy = true;
collapseDnsRecords();
showExpectedDnsRecords(function (error) {
if (error) console.error(error);
@@ -150,20 +159,56 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
}
Client.setMailRelay(data, function (error) {
if (error) $scope.mailRelay.error = error.message;
else $scope.mailRelay.success = true;
$scope.mailRelay.busy = false;
if (error) {
$scope.mailRelay.error = error.message;
return;
}
$scope.currentRelay = data;
$scope.mailRelay.success = true;
$scope.email.refresh();
});
}
};
$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() {
@@ -185,6 +230,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.mailRelay.relay.password = '';
$scope.mailRelay.relay.serverApiToken = '';
$scope.currentRelay = relay;
if (relay.provider === 'postmark-smtp') {
$scope.mailRelay.relay.serverApiToken = relay.username;
} else if (relay.provider === 'sendgrid-smtp') {
@@ -211,25 +258,41 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
});
}
function collapseDnsRecords() {
$scope.expectedDnsRecordsTypes.forEach(function (record) {
var type = record.value;
$('#collapse_dns_' + type).collapse('hide');
});
$('#collapse_outbound_smtp').collapse('hide');
$('#collapse_rbl').collapse('hide');
}
function showExpectedDnsRecords(callback) {
callback = callback || function (error) { if (error) console.error(error); };
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.expectedDnsRecordsTypes.forEach(function (record) {
var type = record.value;
$scope.expectedDnsRecords[type] = result.dns[type] || {};
if (!$scope.expectedDnsRecords[type].status) {
$('#collapse_dns_' + type).collapse('show');
}
}
});
if (!$scope.relay.status) {
$('#collapse_dns_port').collapse('show');
$('#collapse_outbound_smtp').collapse('show');
}
if (!$scope.rbl.status) {
$('#collapse_rbl').collapse('show');
}
callback(null);
@@ -285,5 +348,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();
}]);
+1 -1
View File
@@ -142,7 +142,7 @@
<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>
+2 -2
View File
@@ -515,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';
+1 -1
View File
@@ -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();