Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b2e4ce700 | |||
| 776f184dbc | |||
| a54466f8c2 | |||
| f36641b443 | |||
| 36eb107b83 | |||
| fa16ae9a0c | |||
| 517b36b3f0 | |||
| 83a28afc8f | |||
| e76c7de259 | |||
| fcda4a771c | |||
| 76d8f16e22 | |||
| 1b3cd1f373 | |||
| 62b020e96d | |||
| bc78f4a6d8 | |||
| a8e458e935 | |||
| e4747ef50c | |||
| 0d6637de27 | |||
| 28e513a434 | |||
| e73174685b | |||
| 3af95508f5 | |||
| 4fa8ab596b | |||
| 54c9bb7409 | |||
| 4c7dc5056d | |||
| e986a67d39 | |||
| da8de173a6 | |||
| cbc906f8d1 | |||
| c7958f8e1d | |||
| b88ee8143a | |||
| e413f7ba9b | |||
| 7e1055ae44 | |||
| c61ce40362 | |||
| e48156dceb | |||
| f3811e3df9 | |||
| 0d40b1b80d | |||
| 8b92c8f7ae | |||
| d41eb81b3d | |||
| 3adf91afed | |||
| 18f05de8ae | |||
| b0f4396389 | |||
| bf99475dbd | |||
| d50fa70f47 | |||
| 0e655cadb0 | |||
| 496e1c3dc1 | |||
| 325252699e | |||
| 2d43e22285 | |||
| 9e673c3890 | |||
| c3c18e8a4b | |||
| cb1bd58cb9 | |||
| 0bdff14c9f | |||
| c4ae9526af | |||
| 8d79ac9ae0 | |||
| 7d4ed5bafc | |||
| 85db8f398b | |||
| 636b71ce6f | |||
| b46008f0b1 | |||
| 9d9bd42cd2 | |||
| 0f6a2a42f2 | |||
| fc8bf82993 | |||
| e56192913d | |||
| 2d08ce441f | |||
| 87497c2047 | |||
| 32a0bf6fd2 | |||
| 291e625785 | |||
| 6bcfd33e10 | |||
| b4c15b1719 | |||
| 920626192c | |||
| 778371b818 | |||
| d2a1cea1e8 | |||
| 5683cefe89 | |||
| 126d64ffa8 | |||
| 7262eb208f | |||
| f57f8f5e58 | |||
| 00ad1308aa | |||
| 2cc37a9c31 | |||
| 13b0093f20 | |||
| 5617baea50 | |||
| da79e4f229 | |||
| 6c1bd522e6 | |||
| 7fca9decc1 | |||
| 7cf08b6a4d | |||
| ffedbdfa13 | |||
| 43e207a301 | |||
| bea6019dc4 | |||
| 9042e9e1a9 | |||
| b855dee4cb | |||
| b322f6805f | |||
| ccc119ddec | |||
| 994cbaa22a | |||
| d7a34bbf68 | |||
| 1f31fe6f8f | |||
| 37bdd2672b | |||
| 63702f836a | |||
| 5898428a6c | |||
| f4a6c64956 | |||
| f9d4d3014d | |||
| 8254337552 | |||
| d811115f21 | |||
| fec388b648 | |||
| a969e323a6 | |||
| 09584ac29c | |||
| 7967610f3f | |||
| 727332fe66 | |||
| 09595d1c43 | |||
| c4ad6c803f | |||
| fd1a00d280 | |||
| 5c2a650681 | |||
| 43051cea3b | |||
| 3d50a251ee | |||
| 219df8babd | |||
| d3d9706a70 | |||
| 8a3ad6c964 | |||
| 90719cd4d9 | |||
| 30445ddab9 | |||
| 157fbc89b8 | |||
| 71219c6af7 | |||
| 934abafbd4 | |||
| bc6e896507 | |||
| ca8731c282 | |||
| c511019d79 | |||
| 992c4ee847 | |||
| 8c427553ba | |||
| c1df22f079 | |||
| 7673ecde2f | |||
| a9d0cf66fd | |||
| db89784af8 | |||
| 4143f903ad | |||
| 12820db4a5 | |||
| 8837cc5a3c | |||
| f2545e3def | |||
| 7a72bf3f78 | |||
| 87351f04ef | |||
| 4a04e0b52f | |||
| 5945bce00e | |||
| d2a3925e04 | |||
| 9c9f82e2c5 | |||
| 8fd3ff0ccc | |||
| dec2fdb6bb | |||
| c581d2a52c | |||
| e6e748e30d | |||
| 36fddacf5c | |||
| 183c1608a6 | |||
| 51c8f65e8d | |||
| 0da6e9a5b9 | |||
| 31c7a17684 | |||
| a1e2cd438e | |||
| 6b0e00e28b | |||
| 19948851e0 | |||
| bfe4f75881 | |||
| 7f13594f01 | |||
| 4fafac035e | |||
| ca41e6acfd | |||
| 9893dd6640 | |||
| 8f7e4c2053 | |||
| d037b13401 | |||
| 83c955d25b | |||
| 0bb6d969a4 | |||
| 6062a5bdd2 | |||
| 70ab492efa | |||
| aab035f7b9 | |||
| 0e825272ae | |||
| 46fee9e431 | |||
| 0789c96992 | |||
| a4adc581fa | |||
| 500fb452e7 | |||
| e11b762ea1 | |||
| f5d1726352 | |||
| 3d5aa9fd23 | |||
| ef12740060 | |||
| 415902d68e | |||
| 0ef0e010a3 | |||
| 2d27da89d2 | |||
| 9d8def8349 | |||
| 2533111bfa | |||
| 20d6da8230 | |||
| f159cacfbb | |||
| 5e9ea98b66 | |||
| d87b7dcb75 | |||
| 6eea2fef9a | |||
| 34fd5f14a5 | |||
| a4e73e747c | |||
| eadff099eb | |||
| 15653cb3f8 | |||
| 2f8dc35c5d | |||
| a97720d204 | |||
| 73898505b0 | |||
| 88b4b6a38b | |||
| 3da82e3a63 | |||
| dad1585704 | |||
| e81dbdb36c | |||
| ee2478e500 | |||
| 0f7a6964a4 | |||
| 5fa974ffe6 | |||
| e1b7198a29 | |||
| 37d6354627 | |||
| 6ab3e04fc1 | |||
| b1987868be | |||
| 72eb3007c4 | |||
| 6c1da45ad1 | |||
| b9857cdb65 | |||
| d5c251115c | |||
| 64c66e248b | |||
| bb53c4f331 | |||
| 3215d4a3c9 | |||
| 68c4d77494 | |||
| 44bf299e10 | |||
| 6b1e14b464 | |||
| 8dcde84c3c | |||
| a0deedb958 | |||
| a2096bec18 | |||
| 4f82bcec43 | |||
| 491356ce8d | |||
| 6c99105a7e | |||
| 71f847776b | |||
| 87c5371603 | |||
| 01d676628d | |||
| 60badce935 | |||
| 182ae6bf1f | |||
| c62ef9e156 | |||
| 96383a1fae | |||
| 5e9542ee76 | |||
| cc28d49df4 | |||
| 18f3733d6e | |||
| 87dcf42c7e | |||
| 32d8627045 | |||
| 6a607f9565 | |||
| c623770b44 | |||
| 69f3620b22 | |||
| 21110bb2e0 | |||
| fabe55622e | |||
| 73e079cc6c | |||
| a7d22a1972 | |||
| 5c1970b37f | |||
| db065bd0fc | |||
| db6d8deec4 | |||
| 414b21f29a |
@@ -468,3 +468,45 @@
|
||||
- Fix crash when user attempts to set a duplicate email
|
||||
- Improved mongodb crash recovery
|
||||
|
||||
[0.12.1]
|
||||
- Fix crash when backing up apps
|
||||
|
||||
[0.12.2]
|
||||
- Improved error handling for addons
|
||||
|
||||
[0.12.3]
|
||||
- LDAP: Do not set sn attribute when user has no surname
|
||||
|
||||
[0.12.4]
|
||||
- Install app only after platform is ready
|
||||
|
||||
[0.12.5]
|
||||
- Get alerts for app task failures
|
||||
- Fix update issue when one or more apps are in failed state
|
||||
|
||||
[0.12.6]
|
||||
- Allow setting an alternate external domain for apps
|
||||
|
||||
[0.12.7]
|
||||
- Fix changing password
|
||||
|
||||
[0.13.0]
|
||||
- Upgrade to ubuntu 16.04
|
||||
- Add event log
|
||||
|
||||
[0.13.1]
|
||||
- Make activity log viewable to admins
|
||||
- Fix geoip lookup
|
||||
|
||||
[0.13.2]
|
||||
- Fix crash in app auto updater
|
||||
- Fix crash with empty timezone
|
||||
|
||||
[0.13.3]
|
||||
- Enable auth in email addon
|
||||
- Add search for activity log
|
||||
- Add tutorial for first time users
|
||||
|
||||
[0.13.4]
|
||||
- Fix mail addon restart issue
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ while true; do
|
||||
done
|
||||
|
||||
echo "Copying INFRA_VERSION"
|
||||
$scp22 "${SCRIPT_DIR}/../setup/INFRA_VERSION" root@${server_ip}:.
|
||||
$scp22 "${SCRIPT_DIR}/../src/INFRA_VERSION" root@${server_ip}:.
|
||||
|
||||
echo "Copying box source"
|
||||
cd "${SOURCE_DIR}"
|
||||
|
||||
@@ -30,7 +30,7 @@ function create_droplet() {
|
||||
local box_name="$2"
|
||||
|
||||
local image_region="sfo1"
|
||||
local ubuntu_image_slug="ubuntu-15-10-x64"
|
||||
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||
local box_size="512mb"
|
||||
|
||||
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||
|
||||
@@ -17,7 +17,7 @@ function die {
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
|
||||
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
|
||||
|
||||
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
|
||||
source "${SOURCE_DIR}/INFRA_VERSION"
|
||||
@@ -183,7 +183,7 @@ fi
|
||||
|
||||
echo "==== Install nginx ===="
|
||||
apt-get -y install nginx-full
|
||||
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
|
||||
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
|
||||
|
||||
echo "==== Install build-essential ===="
|
||||
apt-get -y install build-essential rcconf
|
||||
@@ -191,8 +191,8 @@ apt-get -y install build-essential rcconf
|
||||
echo "==== Install mysql ===="
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
apt-get -y install mysql-server
|
||||
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
|
||||
apt-get -y install mysql-server-5.7
|
||||
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
|
||||
|
||||
echo "==== Install pwgen and swaks awscli ===="
|
||||
apt-get -y install pwgen swaks awscli
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
mailer = require('./src/mailer.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
|
||||
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
|
||||
|
||||
function collectLogs(program, callback) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
function sendCrashNotification(processName) {
|
||||
collectLogs(processName, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
result = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
console.log('Sending crash notification email for', processName);
|
||||
mailer.sendCrashNotification(processName, result);
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
sendCrashNotification(processName);
|
||||
}
|
||||
|
||||
main();
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
|
||||
function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE eventlog(" +
|
||||
"id VARCHAR(128) NOT NULL," +
|
||||
"source JSON," +
|
||||
"creationTime TIMESTAMP," +
|
||||
"action VARCHAR(128) NOT NULL," +
|
||||
"data JSON," +
|
||||
"PRIMARY KEY (id))";
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE eventlog', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
+12
-1
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
showTutorial BOOLEAN DEFAULT 0,
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
@@ -65,6 +66,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
oauthProxy BOOLEAN DEFAULT 0,
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
altDomain VARCHAR(256),
|
||||
|
||||
lastBackupId VARCHAR(128),
|
||||
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
||||
@@ -99,7 +101,7 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
filename VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
@@ -107,3 +109,12 @@ CREATE TABLE IF NOT EXISTS backups(
|
||||
state VARCHAR(16) NOT NULL,
|
||||
|
||||
PRIMARY KEY (filename));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eventlog(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
action VARCHAR(128) NOT NULL,
|
||||
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
|
||||
data JSON, /* free flowing json based on action */
|
||||
creationTime TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
Generated
+969
-790
File diff suppressed because it is too large
Load Diff
+8
-10
@@ -14,10 +14,9 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"attempt": "^1.0.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"bytes": "^2.1.0",
|
||||
"bytes": "^2.3.0",
|
||||
"cloudron-manifestformat": "^2.3.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
@@ -28,17 +27,16 @@
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.9.2",
|
||||
"debug": "^2.2.0",
|
||||
"dockerode": "^2.2.2",
|
||||
"dockerode": "^2.2.10",
|
||||
"ejs": "^2.2.4",
|
||||
"ejs-cli": "^1.0.1",
|
||||
"ejs-cli": "^1.2.0",
|
||||
"express": "^4.12.4",
|
||||
"express-session": "^1.11.3",
|
||||
"hat": "0.0.3",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^0.7.1",
|
||||
"memorystream": "^0.3.0",
|
||||
"mime": "^1.3.4",
|
||||
"morgan": "^1.6.0",
|
||||
"morgan": "^1.7.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"native-dns": "^0.7.0",
|
||||
@@ -60,15 +58,15 @@
|
||||
"semver": "^4.3.6",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^1.5.0",
|
||||
"superagent": "^1.8.3",
|
||||
"supererror": "^0.7.1",
|
||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
"tldjs": "^1.6.2",
|
||||
"underscore": "^1.7.0",
|
||||
"ursa": "^0.9.1",
|
||||
"ursa": "^0.9.3",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^4.4.0",
|
||||
"x509": "^0.2.2"
|
||||
"validator": "^4.9.0",
|
||||
"x509": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "*",
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
# http://bugs.mysql.com/bug.php?id=68514
|
||||
[mysqld]
|
||||
performance_schema=OFF
|
||||
max_connection=50
|
||||
max_connections=50
|
||||
|
||||
@@ -31,3 +31,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/setup_infra.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setup_infra.sh
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ StopWhenUnneeded=false
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
ExecStart="/home/yellowtent/box/crashnotifier.js" %I
|
||||
ExecStart="/home/yellowtent/box/crashnotifierservice.js" %I
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
|
||||
readonly DATA_DIR="/home/yellowtent/data"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
|
||||
source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION
|
||||
source "${script_dir}/../src/INFRA_VERSION" # this injects INFRA_VERSION
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
|
||||
+1
-4
@@ -39,7 +39,7 @@ set_progress "10" "Ensuring directories"
|
||||
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
|
||||
mkdir -p "${DATA_DIR}/box/appicons"
|
||||
mkdir -p "${DATA_DIR}/box/certs"
|
||||
mkdir -p "${DATA_DIR}/box/mail"
|
||||
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
|
||||
mkdir -p "${DATA_DIR}/box/acme" # acme keys
|
||||
mkdir -p "${DATA_DIR}/graphite"
|
||||
|
||||
@@ -122,9 +122,6 @@ set_progress "33" "Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
set_progress "40" "Setting up infra"
|
||||
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
|
||||
|
||||
set_progress "65" "Creating cloudron.conf"
|
||||
sudo -u yellowtent -H bash <<EOF
|
||||
set -eu
|
||||
|
||||
@@ -18,9 +18,10 @@ server {
|
||||
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
|
||||
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
# https://cipherli.st/
|
||||
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
||||
ssl_ciphers 'AES128+EECDH:AES128+EDH';
|
||||
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=27
|
||||
INFRA_VERSION=30
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
@@ -12,7 +12,7 @@ MYSQL_IMAGE=cloudron/mysql:0.11.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.9.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.9.0
|
||||
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.10.0
|
||||
MAIL_IMAGE=cloudron/mail:0.12.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
|
||||
|
||||
MYSQL_REPO=cloudron/mysql
|
||||
+60
-180
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
|
||||
setupAddons: setupAddons,
|
||||
teardownAddons: teardownAddons,
|
||||
backupAddons: backupAddons,
|
||||
@@ -19,22 +21,21 @@ exports = module.exports = {
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
certificates = require('./certificates.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js').connection,
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
fs = require('fs'),
|
||||
generatePassword = require('password-generator'),
|
||||
hat = require('hat'),
|
||||
MemoryStream = require('memorystream'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
@@ -111,7 +112,8 @@ var KNOWN_ADDONS = {
|
||||
}
|
||||
};
|
||||
|
||||
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
|
||||
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
SETUP_INFRA_CMD = path.join(__dirname, 'scripts/setup_infra.sh');;
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
@@ -120,6 +122,17 @@ function debugApp(app, args) {
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function initialize(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
debug('initializing addon infrastructure');
|
||||
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
shell.sudo('seutp_infra', [ SETUP_INFRA_CMD, config.fqdn(), config.adminFqdn(), certFilePath, keyFilePath ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupAddons(app, addons, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
@@ -388,13 +401,14 @@ function setupSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
|
||||
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
|
||||
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
|
||||
'MAIL_SMTP_USERNAME=' + username,
|
||||
'MAIL_SMTP_PASSWORD=' + hat(256), // this is ignored
|
||||
'MAIL_SMTP_USERNAME=' + from, // change this to app.id after apps have moved
|
||||
'MAIL_SMTP_PASSWORD=' + 'app-' + app.id, // this is ignored
|
||||
'MAIL_FROM=' + from + '@' + config.fqdn(),
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
|
||||
@@ -420,31 +434,14 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
});
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -453,24 +450,14 @@ function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('mysql', cmd, { }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'mysql', callback);
|
||||
});
|
||||
});
|
||||
appdb.unsetAddonConfig(app.id, 'mysql', callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -482,15 +469,9 @@ function backupMySql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupMySql failed with status ' + code : null);
|
||||
});
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
docker.execContainer('mysql', cmd, { stdout: output }, callback);
|
||||
}
|
||||
|
||||
function restoreMySql(app, options, callback) {
|
||||
@@ -504,17 +485,8 @@ function restoreMySql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restoreMySql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restoreMySql failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
|
||||
docker.execContainer('mysql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -525,31 +497,14 @@ function setupPostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up postgresql');
|
||||
|
||||
var container = docker.getContainer('postgresql');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
});
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -558,24 +513,14 @@ function teardownPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('postgresql');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down postgresql');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('postgresql', cmd, { }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
|
||||
});
|
||||
});
|
||||
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -587,19 +532,13 @@ function backupPostgreSql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'postgresql', '/addons/postgresql/service.sh', 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupPostgreSql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupPostgreSql failed with status ' + code : null);
|
||||
});
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
|
||||
}
|
||||
|
||||
function restorePostgreSql(app, options, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
callback = once(callback);
|
||||
|
||||
setupPostgreSql(app, options, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -609,17 +548,9 @@ function restorePostgreSql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'postgresql', '/addons/postgresql/service.sh', 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restorePostgreSql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restorePostgreSql failed with status ' + code : null);
|
||||
});
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -630,31 +561,14 @@ function setupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
var container = docker.getContainer('mongodb');
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
});
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -663,24 +577,14 @@ function teardownMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('mongodb');
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
docker.execContainer('mongodb', cmd, { }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
|
||||
});
|
||||
});
|
||||
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -692,15 +596,9 @@ function backupMongoDb(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mongodb', '/addons/mongodb/service.sh', 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupMongoDb: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupMongoDb failed with status ' + code : null);
|
||||
});
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
|
||||
}
|
||||
|
||||
function restoreMongoDb(app, options, callback) {
|
||||
@@ -714,26 +612,16 @@ function restoreMongoDb(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mongodb', '/addons/mongodb/service.sh', 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restoreMongoDb: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restoreMongoDb failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
|
||||
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function forwardRedisPort(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.getContainer('redis-' + appId).inspect(function (error, data) {
|
||||
dockerConnection.getContainer('redis-' + appId).inspect(function (error, data) {
|
||||
if (error) return callback(new Error('Unable to inspect container:' + error));
|
||||
|
||||
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
|
||||
@@ -781,7 +669,7 @@ function setupRedis(app, options, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: 'redis-' + app.location,
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Image: 'cloudron/redis:0.8.0', // if you change this, fix src/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {
|
||||
'/tmp': {},
|
||||
@@ -813,9 +701,9 @@ function setupRedis(app, options, callback) {
|
||||
'REDIS_PORT=6379'
|
||||
];
|
||||
|
||||
var redisContainer = docker.getContainer(createOptions.name);
|
||||
var redisContainer = dockerConnection.getContainer(createOptions.name);
|
||||
stopAndRemoveRedis(redisContainer, function () {
|
||||
docker.createContainer(createOptions, function (error) {
|
||||
dockerConnection.createContainer(createOptions, function (error) {
|
||||
if (error && error.statusCode !== 409) return callback(error); // if not already created
|
||||
|
||||
redisContainer.start(function (error) {
|
||||
@@ -836,7 +724,7 @@ function teardownRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('redis-' + app.id);
|
||||
var container = dockerConnection.getContainer('redis-' + app.id);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
@@ -859,15 +747,7 @@ function teardownRedis(app, options, callback) {
|
||||
function backupRedis(app, options, callback) {
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
docker.execContainer('redis-' + app.id, cmd, { }, callback);
|
||||
}
|
||||
|
||||
+5
-21
@@ -4,7 +4,6 @@
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getByHttpPort: getByHttpPort,
|
||||
getByContainerId: getByContainerId,
|
||||
add: add,
|
||||
@@ -59,7 +58,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.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit' ].join(',');
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -114,22 +113,6 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBySubdomain(subdomain, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof httpPort, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -177,7 +160,7 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, callback) {
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, altDomain, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
@@ -186,6 +169,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert(altDomain === null || typeof altDomain === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
portBindings = portBindings || { };
|
||||
@@ -195,8 +179,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit, altDomain ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
|
||||
@@ -162,7 +162,7 @@ function processDockerEvents() {
|
||||
debug('OOM Context: %s', context);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
|
||||
if (error || app.appStoreId !== '') mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+47
-32
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
hasAccessTo: hasAccessTo,
|
||||
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getByIpAddress: getByIpAddress,
|
||||
getAll: getAll,
|
||||
getAllByUser: getAllByUser,
|
||||
@@ -50,6 +49,7 @@ var addons = require('./addons.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
docker = require('./docker.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
@@ -103,7 +103,7 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name
|
||||
function validateHostname(location, fqdn) {
|
||||
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION ];
|
||||
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION ];
|
||||
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
|
||||
|
||||
@@ -126,13 +126,17 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
25, /* smtp */
|
||||
53, /* dns */
|
||||
80, /* http */
|
||||
143, /* imap */
|
||||
443, /* https */
|
||||
465, /* smtps */
|
||||
587, /* submission */
|
||||
919, /* ssh */
|
||||
993, /* imaps */
|
||||
2003, /* graphite (lo) */
|
||||
2004, /* graphite (lo) */
|
||||
2020, /* install server */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('internalPort'), /* internal app server (lo) */
|
||||
config.get('sysadminPort'), /* sysadmin app server (lo) */
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
|
||||
config.get('simpleAuthPort'), /* simple auth server (lo) */
|
||||
@@ -261,22 +265,7 @@ function get(appId, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getBySubdomain(subdomain, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getBySubdomain(subdomain, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
@@ -294,7 +283,7 @@ function getByIpAddress(ip, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
@@ -309,7 +298,7 @@ function getAll(callback) {
|
||||
|
||||
apps.forEach(function (app) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
||||
});
|
||||
|
||||
callback(null, apps);
|
||||
@@ -353,7 +342,7 @@ function purchase(appStoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, callback) {
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
@@ -364,6 +353,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert(altDomain === null || typeof altDomain === 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = manifestFormat.parse(manifest);
|
||||
@@ -387,6 +378,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
// memoryLimit might come in as 0 if not specified
|
||||
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
|
||||
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
||||
|
||||
// singleUser mode requires accessRestriction to contain exactly one user
|
||||
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
@@ -407,7 +400,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
purchase(appStoreId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, function (error) {
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, altDomain, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -419,12 +412,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, callback) {
|
||||
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, altDomain, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -432,6 +427,8 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert(altDomain === null || typeof altDomain === 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateHostname(location, config.fqdn());
|
||||
@@ -443,6 +440,8 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -467,12 +466,14 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
|
||||
accessRestriction: accessRestriction,
|
||||
portBindings: portBindings,
|
||||
memoryLimit: memoryLimit,
|
||||
altDomain: altDomain,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit
|
||||
memoryLimit: app.memoryLimit,
|
||||
altDomain: altDomain
|
||||
}
|
||||
};
|
||||
|
||||
@@ -485,17 +486,20 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
function update(appId, force, manifest, portBindings, icon, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof force, 'boolean');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert(!portBindings || typeof portBindings === 'object');
|
||||
assert(typeof portBindings === 'object'); // can be null
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will update app with id:%s', appId);
|
||||
@@ -544,7 +548,8 @@ function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction,
|
||||
memoryLimit: app.memoryLimit
|
||||
memoryLimit: app.memoryLimit,
|
||||
altDomain: app.altDomain
|
||||
}
|
||||
};
|
||||
|
||||
@@ -555,6 +560,8 @@ function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
@@ -605,8 +612,9 @@ function getLogs(appId, lines, follow, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restore(appId, callback) {
|
||||
function restore(appId, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will restore app with id:%s', appId);
|
||||
@@ -637,7 +645,8 @@ function restore(appId, callback) {
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
manifest: app.manifest
|
||||
manifest: app.manifest,
|
||||
altDomain: app.altDomain
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -648,13 +657,16 @@ function restore(appId, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(appId, callback) {
|
||||
function uninstall(appId, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will uninstall app with id:%s', appId);
|
||||
@@ -664,6 +676,8 @@ function uninstall(appId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
||||
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
});
|
||||
@@ -796,7 +810,8 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
|
||||
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings,
|
||||
null /* icon */, { userId: null, username: 'autoupdater' }, function (error) {
|
||||
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
|
||||
|
||||
iteratorDone(null);
|
||||
|
||||
+40
-28
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -19,7 +17,8 @@ exports = module.exports = {
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
_waitForDnsPropagation: waitForDnsPropagation,
|
||||
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
@@ -59,6 +58,7 @@ var addons = require('./addons.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
@@ -101,9 +101,7 @@ function configureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
|
||||
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
@@ -232,17 +230,19 @@ function downloadIcon(app, callback) {
|
||||
|
||||
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
|
||||
|
||||
superagent
|
||||
.get(iconUrl)
|
||||
.buffer(true)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) return callback(new Error('Network error downloading icon:' + error.message));
|
||||
if (res.statusCode !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
superagent
|
||||
.get(iconUrl)
|
||||
.buffer(true)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) return retryCallback(new Error('Network error downloading icon:' + error.message));
|
||||
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
retryCallback(null);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function registerSubdomain(app, callback) {
|
||||
@@ -319,20 +319,23 @@ function waitForDnsPropagation(app, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function retry(error) {
|
||||
debugApp(app, 'waitForDnsPropagation: ', error);
|
||||
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
|
||||
}
|
||||
async.retry({ interval: 5000, times: 120 }, function checkStatus(retryCallback) {
|
||||
subdomains.status(app.dnsRecordId, function (error, result) {
|
||||
if (error) return retryCallback(new Error('Failed to get dns record status : ' + error.message));
|
||||
|
||||
subdomains.status(app.dnsRecordId, function (error, result) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
|
||||
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
|
||||
if (result !== 'done') return retryCallback(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
|
||||
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
retryCallback(null, result);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
function waitForAltDomainDnsPropagation(app, callback) {
|
||||
if (!app.altDomain) return callback(null);
|
||||
|
||||
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', callback); // waits forever
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -411,9 +414,12 @@ function install(app, callback) {
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
@@ -512,9 +518,12 @@ function restore(app, callback) {
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
@@ -574,6 +583,9 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for Alt Domain DNS propagation' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
@@ -630,7 +642,7 @@ function update(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
|
||||
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
|
||||
backups.backupApp.bind(null, app, app.oldConfig.manifest.addons)
|
||||
], next);
|
||||
},
|
||||
|
||||
|
||||
+23
-11
@@ -27,6 +27,7 @@ var addons = require('./addons.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
@@ -189,18 +190,23 @@ function copyLastBackup(app, callback) {
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
var now = new Date();
|
||||
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), app.manifest.version);
|
||||
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), app.manifest.version);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('copyLastBackup: copying archive %s to %s', app.lastBackupId, toFilenameArchive);
|
||||
|
||||
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
|
||||
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
|
||||
|
||||
debug('copyLastBackup: copying config %s to %s', configFileId, toFilenameConfig);
|
||||
|
||||
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
@@ -242,10 +248,10 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
// function backupBox(callback) {
|
||||
// apps.getAll(function (error, allApps) {
|
||||
// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
//
|
||||
//
|
||||
// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
|
||||
// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
||||
//
|
||||
//
|
||||
// backupBoxWithAppBackupIds(appBackupIds, callback);
|
||||
// });
|
||||
// }
|
||||
@@ -362,9 +368,13 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
}
|
||||
|
||||
// this function expects you to have a lock
|
||||
function backupBoxAndApps(callback) {
|
||||
function backupBoxAndApps(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -397,23 +407,24 @@ function backupBoxAndApps(callback) {
|
||||
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
|
||||
|
||||
callback(error, filename);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backup(callback) {
|
||||
function backup(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
|
||||
|
||||
// ensure tools can 'wait' on progress
|
||||
progress.set(progress.BACKUP, 0, 'Starting');
|
||||
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
|
||||
if (error) console.error('backup failed.', error);
|
||||
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
@@ -436,7 +447,8 @@ function ensureBackup(callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(callback);
|
||||
var eventSource = { userId: null, username: 'cron' };
|
||||
backup(eventSource, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+57
-28
@@ -1,5 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
installAdminCertificate: installAdminCertificate,
|
||||
autoRenew: autoRenew,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
CertificatesError: CertificatesError,
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
getAdminCertificatePath: getAdminCertificatePath
|
||||
};
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
@@ -9,6 +20,7 @@ var acme = require('./cert/acme.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/certificates'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
nginx = require('./nginx.js'),
|
||||
@@ -17,22 +29,11 @@ var acme = require('./cert/acme.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
user = require('./user.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
x509 = require('x509');
|
||||
|
||||
exports = module.exports = {
|
||||
installAdminCertificate: installAdminCertificate,
|
||||
autoRenew: autoRenew,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
CertificatesError: CertificatesError,
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
@@ -56,12 +57,16 @@ function CertificatesError(reason, errorOrMessage) {
|
||||
util.inherits(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
CertificatesError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function getApi(callback) {
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
||||
var api = !app.altDomain && tlsConfig.provider === 'caas' ? caas : acme;
|
||||
|
||||
var options = { };
|
||||
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
@@ -89,11 +94,10 @@ function installAdminCertificate(callback) {
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var zoneName = tld.getDomain(config.fqdn());
|
||||
waitForDns(config.adminFqdn(), ip, zoneName, function (error) {
|
||||
waitForDns(config.adminFqdn(), ip, 'A', function (error) {
|
||||
if (error) return callback(error); // this cannot happen because we retry forever
|
||||
|
||||
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
||||
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) { // currently, this can never happen
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
@@ -126,11 +130,11 @@ function autoRenew(callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ location: 'my' }); // inject fake webadmin app
|
||||
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
|
||||
|
||||
var expiringApps = [ ];
|
||||
for (var i = 0; i < allApps.length; i++) {
|
||||
var appDomain = config.appFqdn(allApps[i].location);
|
||||
var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location);
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
|
||||
|
||||
@@ -144,20 +148,23 @@ function autoRenew(callback) {
|
||||
}
|
||||
}
|
||||
|
||||
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return config.appFqdn(a.location); }));
|
||||
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
|
||||
|
||||
getApi(function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
||||
var domain = config.appFqdn(app.location);
|
||||
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
|
||||
|
||||
api.getCertificate(domain, apiOptions, function (error) {
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
mailer.certificateRenewed(domain, error ? error.message : '');
|
||||
var errorMessage = error ? error.message : '';
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, { userId: null, username: 'cron' }, { domain: domain, errorMessage: errorMessage });
|
||||
mailer.certificateRenewed(domain, errorMessage);
|
||||
|
||||
if (error) {
|
||||
debug('autoRenew: could not renew cert for %s because %s', domain, error);
|
||||
@@ -252,12 +259,20 @@ function setFallbackCertificate(cert, key, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackCertificatePath(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// any user fallback cert is always copied over to nginx cert dir
|
||||
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||
}
|
||||
|
||||
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
@@ -271,10 +286,24 @@ function setAdminCertificate(cert, key, callback) {
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function getAdminCertificatePath(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
getFallbackCertificatePath(callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
|
||||
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
@@ -287,7 +316,7 @@ function ensureCertificate(domain, callback) {
|
||||
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
|
||||
getApi(function (error, api, apiOptions) {
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
|
||||
|
||||
+19
-2
@@ -8,7 +8,14 @@ exports = module.exports = {
|
||||
del: del,
|
||||
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
|
||||
getClientTokensByUserId: getClientTokensByUserId,
|
||||
delClientTokensByUserId: delClientTokensByUserId
|
||||
delClientTokensByUserId: delClientTokensByUserId,
|
||||
|
||||
SCOPE_APPS: 'apps',
|
||||
SCOPE_DEVELOPER: 'developer',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_ROOT: 'root',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_USERS: 'users'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -47,10 +54,20 @@ ClientsError.INVALID_CLIENT = 'Invalid client';
|
||||
function validateScope(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
var VALID_SCOPES = [
|
||||
exports.SCOPE_APPS,
|
||||
exports.SCOPE_DEVELOPER,
|
||||
exports.SCOPE_PROFILE,
|
||||
exports.SCOPE_ROOT,
|
||||
exports.SCOPE_SETTINGS,
|
||||
exports.SCOPE_USERS
|
||||
];
|
||||
|
||||
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
|
||||
if (scope === '*') return null;
|
||||
|
||||
// TODO maybe validate all individual scopes if they exist
|
||||
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
|
||||
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+41
-11
@@ -35,6 +35,7 @@ var apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
df = require('node-df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
@@ -101,6 +102,8 @@ CloudronError.NOT_FOUND = 'Not found';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
ensureDkimKeySync();
|
||||
|
||||
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
|
||||
|
||||
@@ -190,36 +193,43 @@ function setTimeZone(ip, callback) {
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
|
||||
// https://github.com/bluesmoon/node-geoip
|
||||
// https://github.com/runk/node-maxmind
|
||||
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
|
||||
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
|
||||
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
|
||||
|
||||
superagent.get('http://freegeoip.net/json/' + ip).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (!result.body.timezone) {
|
||||
if (!result.body.time_zone || typeof result.body.time_zone !== 'string') {
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', result.body.timezone);
|
||||
debug('Setting timezone to ', result.body.time_zone);
|
||||
|
||||
settings.setTimeZone(result.body.timezone, callback);
|
||||
settings.setTimeZone(result.body.time_zone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, displayName, ip, callback) {
|
||||
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
user.createOwner(username, password, email, displayName, function (error, userObject) {
|
||||
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
|
||||
@@ -239,6 +249,8 @@ function activate(username, password, email, displayName, ip, callback) {
|
||||
// EE API is sync. do not keep the REST API reponse waiting
|
||||
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
});
|
||||
});
|
||||
@@ -352,6 +364,21 @@ function sendHeartbeat() {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKeySync() {
|
||||
var dkimPrivateKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/private');
|
||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||
|
||||
if (fs.existsSync(dkimPrivateKeyFile) && fs.existsSync(dkimPublicKeyFile)) {
|
||||
debug('DKIM keys already present');
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Generating new DKIM keys');
|
||||
|
||||
safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
|
||||
safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
|
||||
}
|
||||
|
||||
function readDkimPublicKeySync() {
|
||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
@@ -412,7 +439,7 @@ function addDnsRecords() {
|
||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
@@ -510,12 +537,15 @@ function update(boxUpdateInfo, callback) {
|
||||
}
|
||||
|
||||
|
||||
function updateToLatest(callback) {
|
||||
function updateToLatest(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||
|
||||
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
|
||||
|
||||
update(boxUpdateInfo, callback);
|
||||
}
|
||||
|
||||
@@ -539,7 +569,7 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backups.backupBoxAndApps(function (error) {
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
@@ -568,7 +598,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backups.backupBoxAndApps(function (error) {
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
@@ -641,7 +671,7 @@ function installAppBundle(callback) {
|
||||
apps.install(uuid.v4(), appstoreId, result.body.manifest, appInfo.location,
|
||||
appInfo.portBindings || null, appInfo.accessRestriction || null,
|
||||
null /* icon */, null /* cert */, null /* key */, 0 /* default mem limit */,
|
||||
iteratorCallback);
|
||||
null /* altDomain */, { userId: null, username: 'autoinstaller' }, iteratorCallback);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debug('autoInstallApps: ', error);
|
||||
|
||||
+6
-1
@@ -26,6 +26,7 @@ exports = module.exports = {
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // caas routes
|
||||
adminFqdn: adminFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
@@ -77,7 +78,7 @@ function initConfig() {
|
||||
data.version = null;
|
||||
data.isCustomDomain = false;
|
||||
data.webServerOrigin = null;
|
||||
data.internalPort = 3001;
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.oauthProxyPort = 3003;
|
||||
data.simpleAuthPort = 3004;
|
||||
@@ -174,6 +175,10 @@ function internalAdminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('port');
|
||||
}
|
||||
|
||||
function sysadminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
exports = module.exports = {
|
||||
ADMIN_LOCATION: 'my',
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
|
||||
+2
-1
@@ -121,7 +121,8 @@ function clear(callback) {
|
||||
require('./tokendb.js')._clear,
|
||||
require('./groupdb.js')._clear,
|
||||
require('./userdb.js')._clear,
|
||||
require('./settingsdb.js')._clear
|
||||
require('./settingsdb.js')._clear,
|
||||
require('./eventlogdb.js')._clear
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -14,6 +14,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:developer'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -50,18 +51,23 @@ function enabled(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setEnabled(enabled, callback) {
|
||||
function setEnabled(enabled, auditSource, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.setDeveloperMode(enabled, function (error) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function issueDeveloperToken(user, callback) {
|
||||
function issueDeveloperToken(user, auditSource, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
@@ -70,6 +76,8 @@ function issueDeveloperToken(user, callback) {
|
||||
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users,profile', function (error) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
|
||||
|
||||
callback(null, { token: token, expiresAt: expiresAt });
|
||||
});
|
||||
}
|
||||
|
||||
+51
-15
@@ -1,17 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/docker.js'),
|
||||
Docker = require('dockerode'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance(),
|
||||
downloadImage: downloadImage,
|
||||
@@ -25,10 +13,12 @@ exports = module.exports = {
|
||||
deleteImage: deleteImage,
|
||||
deleteContainers: deleteContainers,
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
execContainer: execContainer
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
var Docker = require('dockerode');
|
||||
var docker;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') {
|
||||
@@ -44,6 +34,20 @@ function connectionInstance() {
|
||||
return docker;
|
||||
}
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/docker.js'),
|
||||
once = require('once'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
@@ -134,12 +138,13 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var manifest = app.manifest;
|
||||
var developmentMode = !!manifest.developmentMode;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + config.appFqdn(app.location),
|
||||
'APP_DOMAIN=' + config.appFqdn(app.location)
|
||||
'APP_ORIGIN=https://' + domain,
|
||||
'APP_DOMAIN=' + domain
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
@@ -389,3 +394,34 @@ function getContainerIdByIp(ip, callback) {
|
||||
callback(null, containerId);
|
||||
});
|
||||
}
|
||||
|
||||
function execContainer(containerId, cmd, options, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert(util.isArray(cmd));
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', containerId ].concat(cmd));
|
||||
|
||||
var chunks = [ ];
|
||||
|
||||
if (options.stdout) {
|
||||
cp.stdout.pipe(options.stdout);
|
||||
} else if (options.bufferStdout) {
|
||||
cp.stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
} else {
|
||||
cp.stdout.pipe(process.stdout);
|
||||
}
|
||||
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debug('execContainer code: %s signal: %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'Failed with status ' + code : null, Buffer.concat(chunks));
|
||||
});
|
||||
|
||||
cp.stderr.pipe(options.stderr || process.stderr);
|
||||
|
||||
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
EventLogError: EventLogError,
|
||||
|
||||
add: add,
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
// keep in sync with webadmin index.js filter
|
||||
ACTION_ACTIVATE: 'cloudron.activate',
|
||||
ACTION_APP_CONFIGURE: 'app.configure',
|
||||
ACTION_APP_INSTALL: 'app.install',
|
||||
ACTION_APP_RESTORE: 'app.restore',
|
||||
ACTION_APP_UNINSTALL: 'app.uninstall',
|
||||
ACTION_APP_UPDATE: 'app.update',
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
ACTION_CLI_MODE: 'settings.climode',
|
||||
ACTION_START: 'cloudron.start',
|
||||
ACTION_UPDATE: 'cloudron.update',
|
||||
ACTION_USER_ADD: 'user.add',
|
||||
ACTION_USER_LOGIN: 'user.login',
|
||||
ACTION_USER_REMOVE: 'user.remove',
|
||||
ACTION_USER_UPDATE: 'user.update'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:eventlog'),
|
||||
eventlogdb = require('./eventlogdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function EventLogError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(EventLogError, Error);
|
||||
EventLogError.INTERNAL_ERROR = 'Internal error';
|
||||
EventLogError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function add(action, source, data, callback) {
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof source, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(!callback || typeof callback === 'function');
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var id = uuid.v4();
|
||||
|
||||
eventlogdb.add(id, action, source, data, function (error) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: id });
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new EventLogError(EventLogError.NOT_FOUND, 'No such event'));
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert(typeof search === 'string' || search === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
add: add,
|
||||
count: count,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mysql = require('mysql'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
|
||||
|
||||
// until mysql module supports automatic type coercion
|
||||
function postProcess(eventLog) {
|
||||
eventLog.source = safe.JSON.parse(eventLog.source);
|
||||
eventLog.data = safe.JSON.parse(eventLog.data);
|
||||
return eventLog;
|
||||
}
|
||||
|
||||
function get(eventId, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, postProcess(result[0]));
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert(typeof search === 'string' || search === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [];
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
|
||||
|
||||
if (action || search) query += ' WHERE';
|
||||
if (search) query += ' data LIKE ' + mysql.escape('%' + search + '%');
|
||||
if (action && search) query += ' AND ';
|
||||
|
||||
if (action) {
|
||||
query += ' action=?';
|
||||
data.push(action);
|
||||
}
|
||||
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
|
||||
data.push((page-1)*perPage);
|
||||
data.push(perPage);
|
||||
|
||||
database.query(query, data, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, action, source, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof source, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
database.query('DELETE FROM eventlog', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
+20
-7
@@ -9,6 +9,7 @@ var assert = require('assert'),
|
||||
apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
ldap = require('ldapjs');
|
||||
@@ -62,7 +63,7 @@ function start(callback) {
|
||||
var firstName = nameParts[0];
|
||||
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
|
||||
|
||||
var tmp = {
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['user'],
|
||||
@@ -72,18 +73,21 @@ function start(callback) {
|
||||
mail: entry.email,
|
||||
displayname: displayName,
|
||||
givenName: firstName,
|
||||
sn: lastName,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
|
||||
// http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString
|
||||
// which is required to have atleast one character if present
|
||||
if (lastName.length !== 0) obj.attributes.sn = lastName;
|
||||
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,7 +113,7 @@ function start(callback) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
|
||||
var tmp = {
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['group'],
|
||||
@@ -121,8 +125,8 @@ function start(callback) {
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -130,6 +134,13 @@ function start(callback) {
|
||||
});
|
||||
});
|
||||
|
||||
// this is the bind for the mail addon to authorize apps
|
||||
gServer.bind('ou=sendmail,dc=cloudron', function(req, res, next) {
|
||||
// TODO: validate password
|
||||
debug('sendmail bind: %s', req.dn.toString()); // note: cn can be email or id
|
||||
res.end();
|
||||
});
|
||||
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
// TODO: validate password
|
||||
debug('application bind: %s', req.dn.toString());
|
||||
@@ -174,6 +185,8 @@ function start(callback) {
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id });
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sendFailureLogs: sendFailureLogs
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
mailer = require('./mailer.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
|
||||
var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
|
||||
|
||||
function collectLogs(unitName, callback) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
function sendFailureLogs(processName, options) {
|
||||
assert.strictEqual(typeof processName, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
collectLogs(options.unit || processName, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
result = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
console.log('Sending failure logs for', processName);
|
||||
|
||||
mailer.unexpectedExit(processName, result);
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear Cloudron Team,
|
||||
|
||||
Unfortunately <%= program %> on <%= fqdn %> crashed unexpectedly!
|
||||
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
|
||||
|
||||
Please see some excerpt of the logs below.
|
||||
|
||||
+10
-4
@@ -12,7 +12,7 @@ exports = module.exports = {
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
sendCrashNotification: sendCrashNotification,
|
||||
unexpectedExit: unexpectedExit,
|
||||
|
||||
appDied: appDied,
|
||||
|
||||
@@ -24,6 +24,7 @@ exports = module.exports = {
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
|
||||
FEEDBACK_TYPE_APP_ERROR: 'app_error',
|
||||
FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request',
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
@@ -158,7 +159,11 @@ function sendMails(queue) {
|
||||
|
||||
var transport = nodemailer.createTransport(smtpTransport({
|
||||
host: mailServerIp,
|
||||
port: 2500 // this value comes from mail container
|
||||
port: 2500, // this value comes from mail container
|
||||
auth: {
|
||||
user: 'no-reply', // derive from adminEmail
|
||||
pass: 'supersecret'
|
||||
}
|
||||
}));
|
||||
|
||||
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
|
||||
@@ -394,7 +399,7 @@ function certificateRenewed(domain, message) {
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// crashnotifier should be able to send mail when there is no db
|
||||
function sendCrashNotification(program, context) {
|
||||
function unexpectedExit(program, context) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
@@ -402,7 +407,7 @@ function sendCrashNotification(program, context) {
|
||||
from: config.adminEmail(),
|
||||
to: 'admin@cloudron.io',
|
||||
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
|
||||
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
@@ -417,6 +422,7 @@ function sendFeedback(user, type, subject, description) {
|
||||
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 = {
|
||||
|
||||
+6
-6
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -66,7 +64,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var oauthProxy = requiresOAuthProxy(app);
|
||||
var endpoint = oauthProxy ? 'oauthproxy' : 'app';
|
||||
var vhost = config.appFqdn(app.location);
|
||||
var vhost = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
@@ -80,10 +78,10 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
debug('writing config for "%s" to %s', app.location, nginxConfigFilename);
|
||||
debug('writing config for "%s" to %s', vhost, nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debug('Error creating nginx config for "%s" : %s', app.location, safe.error.message);
|
||||
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
@@ -94,9 +92,11 @@ function unconfigureApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debug('Error removing nginx configuration of "%s": %s', app.location, safe.error.message);
|
||||
debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
|
||||
+14
-20
@@ -1,10 +1,7 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getApp: getApp,
|
||||
getAppBySubdomain: getAppBySubdomain,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
installApp: installApp,
|
||||
@@ -34,6 +31,11 @@ var apps = require('../apps.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function removeInternalAppFields(app) {
|
||||
return {
|
||||
id: app.id,
|
||||
@@ -49,7 +51,8 @@ function removeInternalAppFields(app) {
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
fqdn: app.fqdn,
|
||||
memoryLimit: app.memoryLimit
|
||||
memoryLimit: app.memoryLimit,
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,17 +67,6 @@ function getApp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppBySubdomain(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.subdomain, 'string');
|
||||
|
||||
apps.getBySubdomain(req.params.subdomain, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such subdomain'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, removeInternalAppFields(app)));
|
||||
});
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
@@ -125,13 +117,14 @@ function installApp(req, res, next) {
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
|
||||
// allow tests to provide an appId for testing
|
||||
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
|
||||
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
|
||||
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
@@ -168,10 +161,11 @@ function configureApp(req, res, next) {
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
|
||||
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
|
||||
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
@@ -190,7 +184,7 @@ function restoreApp(req, res, next) {
|
||||
|
||||
debug('Restore app id:%s', req.params.id);
|
||||
|
||||
apps.restore(req.params.id, function (error) {
|
||||
apps.restore(req.params.id, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
@@ -224,7 +218,7 @@ function uninstallApp(req, res, next) {
|
||||
|
||||
debug('Uninstalling app id:%s', req.params.id);
|
||||
|
||||
apps.uninstall(req.params.id, function (error) {
|
||||
apps.uninstall(req.params.id, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -274,7 +268,7 @@ function updateApp(req, res, next) {
|
||||
|
||||
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
|
||||
|
||||
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings, data.icon, function (error) {
|
||||
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings || null, data.icon, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
|
||||
@@ -12,6 +12,11 @@ var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
@@ -30,7 +35,7 @@ function get(req, res, next) {
|
||||
function create(req, res, next) {
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
backups.backup(function (error) {
|
||||
backups.backup(auditSource(req), function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
|
||||
+12
-8
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -15,15 +13,20 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
config = require('../config.js'),
|
||||
progress = require('../progress.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
progress = require('../progress.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
superagent = require('superagent');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating an admin user and activate the cloudron.
|
||||
*
|
||||
@@ -50,7 +53,7 @@ function activate(req, res, next) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
|
||||
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
|
||||
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
|
||||
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
|
||||
@@ -119,7 +122,7 @@ function getConfig(req, res, next) {
|
||||
|
||||
function update(req, res, next) {
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.updateToLatest(function (error) {
|
||||
cloudron.updateToLatest(auditSource(req), function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -134,7 +137,8 @@ function feedback(req, res, next) {
|
||||
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_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app_missing" or "app_error"'));
|
||||
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"'));
|
||||
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'));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -15,6 +13,11 @@ var developer = require('../developer.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function enabled(req, res, next) {
|
||||
developer.enabled(function (error, enabled) {
|
||||
if (enabled) return next();
|
||||
@@ -25,8 +28,9 @@ function enabled(req, res, next) {
|
||||
function setEnabled(req, res, next) {
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be boolean'));
|
||||
|
||||
developer.setEnabled(req.body.enabled, function (error) {
|
||||
developer.setEnabled(req.body.enabled, auditSource(req), function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
@@ -40,7 +44,7 @@ function login(req, res, next) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!user) return next(new HttpError(401, 'Invalid credentials'));
|
||||
|
||||
developer.issueDeveloperToken(user, function (error, result) {
|
||||
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get
|
||||
};
|
||||
|
||||
var eventlog = require('../eventlog.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function get(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
|
||||
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
|
||||
|
||||
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { eventlogs: result }));
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
|
||||
+3
-1
@@ -6,10 +6,12 @@ exports = module.exports = {
|
||||
clients: require('./clients.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
developer: require('./developer.js'),
|
||||
eventlog: require('./eventlog.js'),
|
||||
graphs: require('./graphs.js'),
|
||||
groups: require('./groups.js'),
|
||||
internal: require('./internal.js'),
|
||||
oauth2: require('./oauth2.js'),
|
||||
profile: require('./profile.js'),
|
||||
settings: require('./settings.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
user: require('./user.js')
|
||||
};
|
||||
|
||||
+17
-9
@@ -1,33 +1,36 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
var appdb = require('../appdb'),
|
||||
apps = require('../apps'),
|
||||
assert = require('assert'),
|
||||
authcodedb = require('../authcodedb'),
|
||||
clientdb = require('../clientdb'),
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
DatabaseError = require('../databaseerror'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
hat = require('hat'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('../middleware/index.js'),
|
||||
oauth2orize = require('oauth2orize'),
|
||||
passport = require('passport'),
|
||||
querystring = require('querystring'),
|
||||
util = require('util'),
|
||||
session = require('connect-ensure-login'),
|
||||
tokendb = require('../tokendb'),
|
||||
appdb = require('../appdb'),
|
||||
url = require('url'),
|
||||
user = require('../user.js'),
|
||||
UserError = user.UserError,
|
||||
hat = require('hat');
|
||||
util = require('util');
|
||||
|
||||
function auditSource(req, appId) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { authType: 'oauth', ip: ip, appId: appId };
|
||||
}
|
||||
|
||||
// create OAuth 2.0 server
|
||||
var gServer = oauth2orize.createServer();
|
||||
|
||||
|
||||
// Register serialialization and deserialization functions.
|
||||
//
|
||||
// The client id is stored in the session and can thus be retrieved for each
|
||||
@@ -306,7 +309,7 @@ function accountSetup(req, res, next) {
|
||||
userObject.username = req.body.username;
|
||||
userObject.displayName = req.body.displayName;
|
||||
|
||||
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, function (error) {
|
||||
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -411,7 +414,10 @@ var authorization = [
|
||||
// Handle our different types of oauth clients
|
||||
var type = req.oauth2.client.type;
|
||||
|
||||
if (type === clientdb.TYPE_ADMIN) return next();
|
||||
if (type === clientdb.TYPE_ADMIN) {
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, 'admin'), { userId: req.oauth2.user.id });
|
||||
return next();
|
||||
}
|
||||
if (type === clientdb.TYPE_EXTERNAL) return next();
|
||||
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
|
||||
|
||||
@@ -422,6 +428,8 @@ var authorization = [
|
||||
if (error) return sendError(req, res, 'Internal error');
|
||||
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
update: update,
|
||||
changePassword: changePassword,
|
||||
setShowTutorial: setShowTutorial
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
groups = require('../groups.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
UserError = user.UserError;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
var result = {};
|
||||
result.id = req.user.id;
|
||||
result.tokenType = req.user.tokenType;
|
||||
|
||||
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
|
||||
result.username = req.user.username;
|
||||
result.email = req.user.email;
|
||||
result.displayName = req.user.displayName;
|
||||
result.showTutorial = req.user.showTutorial;
|
||||
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
result.admin = isAdmin;
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
} else {
|
||||
next(new HttpSuccess(200, result));
|
||||
}
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.update(req.user.id, req.user.username, req.body.email || req.user.email, req.body.displayName || req.user.displayName, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
|
||||
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.setPassword(req.user.id, req.body.newPassword, function (error) {
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function setShowTutorial(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.showTutorial !== 'boolean') return next(new HttpError(400, 'showTutorial must be a boolean.'));
|
||||
|
||||
user.setShowTutorial(req.user.id, req.body.showTutorial, function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -18,6 +16,8 @@ exports = module.exports = {
|
||||
getBackupConfig: getBackupConfig,
|
||||
setBackupConfig: setBackupConfig,
|
||||
|
||||
getTimeZone: getTimeZone,
|
||||
|
||||
setCertificate: setCertificate,
|
||||
setAdminCertificate: setAdminCertificate
|
||||
};
|
||||
@@ -71,6 +71,13 @@ function getCloudronName(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeZone(req, res, next) {
|
||||
settings.getTimeZone(function (error, tz) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { timeZone: tz }));
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronAvatar(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ var backups = require('../backups.js'),
|
||||
BackupsError = require('../backups.js').BackupsError,
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = require('../cloudron.js').CloudronError,
|
||||
debug = require('debug')('box:routes/internal'),
|
||||
debug = require('debug')('box:routes/sysadmin'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
@@ -19,7 +19,8 @@ function backup(req, res, next) {
|
||||
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
backups.backup(function (error) {
|
||||
var auditSource = { userId: null, username: 'sysadmin' };
|
||||
backups.backup(auditSource, function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -31,7 +32,8 @@ function update(req, res, next) {
|
||||
debug('triggering update');
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.updateToLatest(function (error) {
|
||||
var auditSource = { userId: null, username: 'sysadmin' };
|
||||
cloudron.updateToLatest(auditSource, function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -42,7 +42,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '10.0.0';
|
||||
var TEST_IMAGE_TAG = '11.0.0';
|
||||
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
|
||||
|
||||
@@ -114,24 +114,20 @@ describe('Apps', function () {
|
||||
before(function (done) {
|
||||
console.log('Starting addons, this can take 10 seconds');
|
||||
|
||||
child_process.exec(__dirname + '/start_addons.sh', function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
|
||||
imageCreated = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, done);
|
||||
});
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
|
||||
imageCreated = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
@@ -446,24 +442,6 @@ describe('Apps', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get appBySubdomain', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.id).to.eql(APP_ID);
|
||||
expect(res.body.installationState).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get invalid app by Subdomain', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot uninstall invalid app', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
@@ -830,6 +808,34 @@ describe('Apps', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('installation - mongodb addon config', function (done) {
|
||||
var appContainer = docker.getContainer(appEntry.containerId);
|
||||
appContainer.inspect(function (error, data) {
|
||||
var mongodbUrl = null;
|
||||
data.Config.Env.forEach(function (env) { if (env.indexOf('MONGODB_URL=') === 0) mongodbUrl = env.split('=')[1]; });
|
||||
expect(mongodbUrl).to.be.ok();
|
||||
|
||||
var urlp = url.parse(mongodbUrl);
|
||||
var username = urlp.auth.split(':')[0];
|
||||
var password = urlp.auth.split(':')[1];
|
||||
var dbname = urlp.path.substr(1);
|
||||
|
||||
expect(data.Config.Env).to.contain('MONGODB_PORT=27017');
|
||||
expect(data.Config.Env).to.contain('MONGODB_HOST=mongodb');
|
||||
expect(data.Config.Env).to.contain('MONGODB_USERNAME=' + username);
|
||||
expect(data.Config.Env).to.contain('MONGODB_PASSWORD=' + password);
|
||||
expect(data.Config.Env).to.contain('MONGODB_DATABASE=' + dbname);
|
||||
|
||||
var cmd = util.format('mongo --quiet -u %s -p %s %s:%s/%s --eval "db.collection.insert({ item: 34 })"',
|
||||
username, password, 'mongodb', 27017, dbname);
|
||||
|
||||
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('installation - scheduler', function (done) {
|
||||
async.retry({ times: 100, interval: 1000 }, function (retryCallback) {
|
||||
if (fs.existsSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env')) return retryCallback();
|
||||
|
||||
@@ -53,7 +53,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
tokendb = require('../../tokendb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var USER_1_ID = null, token_1;
|
||||
|
||||
function setup(done) {
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
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, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
USER_1_ID = res.body.id;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
token_1 = tokendb.generateToken();
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
|
||||
}
|
||||
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Eventlog API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('get', function () {
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for non-admin', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token_1, page: 1, per_page: 10 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for admin', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token, page: 1, per_page: 10 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with action', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.eventlogs.length).to.equal(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with search', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.eventlogs.length).to.equal(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with search', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/eventlog')
|
||||
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.eventlogs.length).to.equal(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -145,7 +145,8 @@ describe('OAuth2', function () {
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var APP_0 = {
|
||||
@@ -155,7 +156,8 @@ describe('OAuth2', function () {
|
||||
location: 'test',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -165,7 +167,8 @@ describe('OAuth2', function () {
|
||||
location: 'test1',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -175,7 +178,8 @@ describe('OAuth2', function () {
|
||||
location: 'test2',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ USER_0.id ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_3 = {
|
||||
@@ -185,7 +189,8 @@ describe('OAuth2', function () {
|
||||
location: 'test3',
|
||||
portBindings: {},
|
||||
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
// unknown app
|
||||
@@ -308,12 +313,12 @@ describe('OAuth2', function () {
|
||||
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
|
||||
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
|
||||
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, APP_0.altDomain),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain),
|
||||
function (callback) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, userObject) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
@@ -1297,7 +1302,8 @@ describe('Password', function () {
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
|
||||
|
||||
describe('Profile API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
var user_0 = null;
|
||||
var token_0;
|
||||
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
database._clear(function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
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, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
|
||||
.end(function (err, res) {
|
||||
expect(err).to.eql(null);
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// stash for later use
|
||||
token_0 = res.body.token;
|
||||
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('get profile', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with invalid token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
expect(result.body.admin).to.be.ok();
|
||||
expect(result.body.showTutorial).to.be.ok();
|
||||
expect(result.body.displayName).to.be.a('string');
|
||||
expect(result.body.password).to.not.be.ok();
|
||||
expect(result.body.salt).to.not.be.ok();
|
||||
|
||||
user_0 = result.body;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with expired token', function (done) {
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() - 2000; // 1 sec
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with invalid token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
expect(result.body.admin).to.be.ok();
|
||||
expect(result.body.showTutorial).to.be.ok();
|
||||
expect(result.body.displayName).to.be.a('string');
|
||||
expect(result.body.password).to.not.be.ok();
|
||||
expect(result.body.salt).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('change email fails due to missing token', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile')
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
|
||||
expect(res.body.admin).to.equal(true);
|
||||
expect(res.body.displayName).to.equal('');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('change displayName succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ displayName: DISPLAY_NAME_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('password change', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing current password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing new password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTutorial change', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing showTutorial', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
|
||||
.query({ access_token: token_0 })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong showTutorial type', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ showTutorial: 'true' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ showTutorial: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.showTutorial).to.not.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
@@ -360,5 +360,17 @@ describe('Settings API', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('time_zone', function () {
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.timeZone).to.be('America/Los_Angeles');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test0',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar', 'someone'] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -40,7 +41,8 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test1',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar', 'someone' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -50,7 +52,8 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test2',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_3 = {
|
||||
@@ -60,7 +63,8 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test3',
|
||||
portBindings: {},
|
||||
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var CLIENT_0 = {
|
||||
@@ -155,10 +159,10 @@ describe('SimpleAuth API', function () {
|
||||
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
|
||||
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
|
||||
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit)
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, APP_0.altDomain),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ set -eu -o pipefail
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
|
||||
source ${SOURCE_DIR}/setup/INFRA_VERSION
|
||||
source ${SOURCE_DIR}/src/INFRA_VERSION
|
||||
|
||||
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
|
||||
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
|
||||
@@ -14,13 +14,8 @@ root_password=secret
|
||||
start_postgresql() {
|
||||
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
|
||||
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/postgresql_vars.sh
|
||||
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
|
||||
fi
|
||||
rm -rf /tmp/postgresql_vars.sh
|
||||
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
|
||||
|
||||
docker rm -f postgresql 2>/dev/null 1>&2 || true
|
||||
|
||||
@@ -32,13 +27,8 @@ start_postgresql() {
|
||||
start_mysql() {
|
||||
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
|
||||
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/mysql_vars.sh
|
||||
echo "${mysql_vars}" > /tmp/mysql_vars.sh
|
||||
fi
|
||||
rm -rf /tmp/mysql_vars.sh
|
||||
echo "${mysql_vars}" > /tmp/mysql_vars.sh
|
||||
|
||||
docker rm -f mysql 2>/dev/null 1>&2 || true
|
||||
|
||||
@@ -50,13 +40,8 @@ start_mysql() {
|
||||
start_mongodb() {
|
||||
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
|
||||
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
|
||||
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/mongodb_vars.sh
|
||||
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
|
||||
fi
|
||||
rm -rf /tmp/mongodb_vars.sh
|
||||
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
|
||||
|
||||
docker rm -f mongodb 2>/dev/null 1>&2 || true
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../../appdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
nock = require('nock');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
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, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Internal API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('backup', function () {
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } });
|
||||
|
||||
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope.isDone()) {
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,7 @@ var config = require('../../config.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js'),
|
||||
userdb = require('../../userdb.js');
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
@@ -24,14 +23,13 @@ var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'USER@foo.bar', EMAIL_2_NEW = 'happy@ME.com';
|
||||
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@FOO.bar';
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
userdb._clear(function (error) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
groups.create('somegroupid', done);
|
||||
@@ -64,7 +62,6 @@ describe('User API', function () {
|
||||
var user_0, user_1, user_2, user_3 = null;
|
||||
var token = null;
|
||||
var token_1 = tokendb.generateToken();
|
||||
var token_2 = tokendb.generateToken();
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
@@ -391,26 +388,14 @@ describe('User API', function () {
|
||||
user_3 = result.body;
|
||||
|
||||
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
|
||||
checkMails(3, function () {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + user_2.id, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
checkMails(3, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('second user userInfo fails for first user', function (done) {
|
||||
it('get userInfo succeeds for second user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('second user userInfo succeeds for second user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
|
||||
.query({ access_token: token_2 })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_2.toLowerCase());
|
||||
@@ -433,7 +418,7 @@ describe('User API', function () {
|
||||
|
||||
it('list users fails for normal user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token_2 })
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -541,16 +526,6 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('change email for other user fails', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
|
||||
.query({ access_token: token_2 })
|
||||
.send({ email: 'foobar@bar.baz' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
|
||||
.query({ access_token: token })
|
||||
@@ -561,15 +536,15 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('change email for own user succeeds', function (done) {
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
|
||||
.query({ access_token: token_2 })
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_2_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
|
||||
.query({ access_token: token_2 })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_2.toLowerCase());
|
||||
@@ -623,55 +598,4 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Change password
|
||||
it('change password fails due to missing current password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to missing new password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to wrong password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to invalid password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+19
-68
@@ -1,15 +1,11 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
profile: profile,
|
||||
info: info,
|
||||
get: get,
|
||||
update: update,
|
||||
list: listUser,
|
||||
create: createUser,
|
||||
changePassword: changePassword,
|
||||
remove: removeUser,
|
||||
list: list,
|
||||
create: create,
|
||||
remove: remove,
|
||||
verifyPassword: verifyPassword,
|
||||
requireAdmin: requireAdmin,
|
||||
sendInvite: sendInvite,
|
||||
@@ -25,31 +21,12 @@ var assert = require('assert'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
UserError = user.UserError;
|
||||
|
||||
function profile(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
var result = {};
|
||||
result.id = req.user.id;
|
||||
result.tokenType = req.user.tokenType;
|
||||
|
||||
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
|
||||
result.username = req.user.username;
|
||||
result.email = req.user.email;
|
||||
result.displayName = req.user.displayName;
|
||||
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
result.admin = isAdmin;
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
} else {
|
||||
next(new HttpSuccess(200, result));
|
||||
}
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function createUser(req, res, next) {
|
||||
function create(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
@@ -63,7 +40,7 @@ function createUser(req, res, next) {
|
||||
var username = req.body.username || '';
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
user.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
|
||||
user.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
|
||||
@@ -99,7 +76,7 @@ function update(req, res, next) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, function (error) {
|
||||
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
|
||||
@@ -111,33 +88,14 @@ function update(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
|
||||
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.changePassword(req.user.username, req.body.password, req.body.newPassword, function (error) {
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function listUser(req, res, next) {
|
||||
function list(req, res, next) {
|
||||
user.list(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { users: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function info(req, res, next) {
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
@@ -161,7 +119,7 @@ function info(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser(req, res, next) {
|
||||
function remove(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
// rules:
|
||||
@@ -175,7 +133,7 @@ function removeUser(req, res, next) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
user.remove(userObject, function (error) {
|
||||
user.remove(userObject, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -187,24 +145,17 @@ function removeUser(req, res, next) {
|
||||
function verifyPassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
// developers are allowed to through without password
|
||||
// developers are allowed through without password
|
||||
if (req.user.tokenType === tokendb.TYPE_DEV) return next();
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
|
||||
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
user.verifyWithUsername(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// Only allow admins or users, operating on themselves
|
||||
if (req.params.userId && !(req.user.id === req.params.userId || isAdmin)) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.verifyWithUsername(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next();
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ readonly program_name=$1
|
||||
|
||||
echo "${program_name}.log"
|
||||
echo "-------------------"
|
||||
journalctl --all --no-pager -u ${program_name} -n 100
|
||||
journalctl --all --no-pager -u ${program_name} -n 300
|
||||
echo
|
||||
echo
|
||||
echo "dmesg"
|
||||
|
||||
@@ -13,6 +13,16 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
/etc/init.d/collectd restart
|
||||
# when restoring the cloudron with many apps, the apptasks rush in to restart
|
||||
# collectd which makes systemd/collectd very unhappy and puts the collectd in
|
||||
# inactive state
|
||||
for i in {1..10}; do
|
||||
echo "Restarting collectd"
|
||||
if systemctl restart collectd; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Failed to reload collectd. Maybe some other apptask is restarting it"
|
||||
sleep $((RANDOM%30))
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
readonly DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
|
||||
|
||||
arg_fqdn="$1"
|
||||
readonly fqdn="$1"
|
||||
readonly mail_fqdn="$2"
|
||||
readonly mail_tls_cert="$3"
|
||||
readonly mail_tls_key="$4"
|
||||
|
||||
# removing containers ensures containers are launched with latest config updates
|
||||
# restore code in appatask does not delete old containers
|
||||
@@ -48,10 +61,12 @@ fi
|
||||
mail_container_id=$(docker run --restart=always -d --name="mail" \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
-h "${arg_fqdn}" \
|
||||
-e "MAIL_SERVER_NAME=${arg_fqdn}" \
|
||||
-e "MAIL_DOMAIN=${arg_fqdn}" \
|
||||
-h "${fqdn}" \
|
||||
-e "MAIL_DOMAIN=${fqdn}" \
|
||||
-e "MAIL_SERVER_NAME=${mail_fqdn}" \
|
||||
-v "${DATA_DIR}/box/mail:/app/data" \
|
||||
-v "${mail_tls_key}:/etc/tls_key.pem:ro" \
|
||||
-v "${mail_tls_cert}:/etc/tls_cert.pem:ro" \
|
||||
--read-only -v /tmp -v /run \
|
||||
"${MAIL_IMAGE}")
|
||||
echo "Mail container id: ${mail_container_id}"
|
||||
@@ -69,7 +84,7 @@ EOF
|
||||
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
|
||||
-m 256m \
|
||||
--memory-swap 512m \
|
||||
-h "${arg_fqdn}" \
|
||||
-h "${fqdn}" \
|
||||
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
|
||||
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run \
|
||||
@@ -87,7 +102,7 @@ EOF
|
||||
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
|
||||
-m 100m \
|
||||
--memory-swap 200m \
|
||||
-h "${arg_fqdn}" \
|
||||
-h "${fqdn}" \
|
||||
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
|
||||
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run \
|
||||
@@ -105,7 +120,7 @@ EOF
|
||||
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
|
||||
-m 100m \
|
||||
--memory-swap 200m \
|
||||
-h "${arg_fqdn}" \
|
||||
-h "${fqdn}" \
|
||||
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
|
||||
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run \
|
||||
+49
-42
@@ -5,14 +5,17 @@ exports = module.exports = {
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var addons = require('./addons.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auth = require('./auth.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
clients = require('./clients.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
cron = require('./cron.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
express = require('express'),
|
||||
http = require('http'),
|
||||
mailer = require('./mailer.js'),
|
||||
@@ -23,7 +26,7 @@ var assert = require('assert'),
|
||||
taskmanager = require('./taskmanager.js');
|
||||
|
||||
var gHttpServer = null;
|
||||
var gInternalHttpServer = null;
|
||||
var gSysadminHttpServer = null;
|
||||
|
||||
function initializeExpressSync() {
|
||||
var app = express();
|
||||
@@ -65,12 +68,12 @@ function initializeExpressSync() {
|
||||
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
|
||||
|
||||
// scope middleware implicitly also adds bearer token verification
|
||||
var rootScope = routes.oauth2.scope('root');
|
||||
var profileScope = routes.oauth2.scope('profile');
|
||||
var usersScope = routes.oauth2.scope('users');
|
||||
var appsScope = routes.oauth2.scope('apps');
|
||||
var developerScope = routes.oauth2.scope('developer');
|
||||
var settingsScope = routes.oauth2.scope('settings');
|
||||
var rootScope = routes.oauth2.scope(clients.SCOPE_ROOT);
|
||||
var profileScope = routes.oauth2.scope(clients.SCOPE_PROFILE);
|
||||
var usersScope = routes.oauth2.scope(clients.SCOPE_USERS);
|
||||
var appsScope = routes.oauth2.scope(clients.SCOPE_APPS);
|
||||
var developerScope = routes.oauth2.scope(clients.SCOPE_DEVELOPER);
|
||||
var settingsScope = routes.oauth2.scope(clients.SCOPE_SETTINGS);
|
||||
|
||||
// csrf protection
|
||||
var csrf = routes.oauth2.csrf;
|
||||
@@ -96,20 +99,21 @@ function initializeExpressSync() {
|
||||
// feedback
|
||||
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
|
||||
|
||||
router.get ('/api/v1/profile', profileScope, routes.user.profile);
|
||||
// profile api, working off the user behind the provided token
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get);
|
||||
router.put ('/api/v1/profile', profileScope, routes.profile.update);
|
||||
router.put ('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
|
||||
router.put ('/api/v1/profile/tutorial', profileScope, routes.profile.setShowTutorial);
|
||||
|
||||
// user routes only for admins
|
||||
router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list);
|
||||
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
|
||||
router.get ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.get);
|
||||
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
|
||||
router.put ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
|
||||
router.put ('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
|
||||
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
|
||||
|
||||
// user routes for admins and users operating on their own account
|
||||
router.get ('/api/v1/users/:userId', usersScope, routes.user.info);
|
||||
router.put ('/api/v1/users/:userId', usersScope, routes.user.update);
|
||||
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
|
||||
|
||||
// Group management
|
||||
router.get ('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.list);
|
||||
router.post('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.create);
|
||||
@@ -158,27 +162,28 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
|
||||
|
||||
// subdomain routes
|
||||
router.get ('/api/v1/subdomains/:subdomain', routes.apps.getAppBySubdomain);
|
||||
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
|
||||
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
|
||||
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAutoupdatePattern);
|
||||
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronName);
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar);
|
||||
router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig);
|
||||
router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setCertificate);
|
||||
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
|
||||
|
||||
// settings routes
|
||||
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.getAutoupdatePattern);
|
||||
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.setAutoupdatePattern);
|
||||
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.settings.getCloudronName);
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
|
||||
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
|
||||
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
|
||||
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
|
||||
// eventlog route
|
||||
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.create);
|
||||
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
|
||||
router.get ('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.create);
|
||||
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
|
||||
|
||||
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
|
||||
httpServer.setTimeout(0);
|
||||
@@ -210,7 +215,7 @@ function initializeExpressSync() {
|
||||
}
|
||||
|
||||
// provides hooks for the 'installer'
|
||||
function initializeInternalExpressSync() {
|
||||
function initializeSysadminExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
@@ -220,7 +225,7 @@ function initializeInternalExpressSync() {
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Internal :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Sysadmin :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
@@ -232,10 +237,10 @@ function initializeInternalExpressSync() {
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// internal routes
|
||||
router.post('/api/v1/backup', routes.internal.backup);
|
||||
router.post('/api/v1/update', routes.internal.update);
|
||||
router.post('/api/v1/retire', routes.internal.retire);
|
||||
// Sysadmin routes
|
||||
router.post('/api/v1/backup', routes.sysadmin.backup);
|
||||
router.post('/api/v1/update', routes.sysadmin.update);
|
||||
router.post('/api/v1/retire', routes.sysadmin.retire);
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
@@ -245,18 +250,20 @@ function start(callback) {
|
||||
assert.strictEqual(gHttpServer, null, 'Server is already up and running.');
|
||||
|
||||
gHttpServer = initializeExpressSync();
|
||||
gInternalHttpServer = initializeInternalExpressSync();
|
||||
gSysadminHttpServer = initializeSysadminExpressSync();
|
||||
|
||||
async.series([
|
||||
auth.initialize,
|
||||
database.initialize,
|
||||
cloudron.initialize, // keep this here because it reads activation state that others depend on
|
||||
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
|
||||
addons.initialize, // starts the addons
|
||||
taskmanager.initialize,
|
||||
mailer.initialize,
|
||||
cron.initialize,
|
||||
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
|
||||
gInternalHttpServer.listen.bind(gInternalHttpServer, config.get('internalPort'), '127.0.0.1')
|
||||
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -273,12 +280,12 @@ function stop(callback) {
|
||||
mailer.uninitialize,
|
||||
database.uninitialize,
|
||||
gHttpServer.close.bind(gHttpServer),
|
||||
gInternalHttpServer.close.bind(gInternalHttpServer)
|
||||
gSysadminHttpServer.close.bind(gSysadminHttpServer)
|
||||
], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
gHttpServer = null;
|
||||
gInternalHttpServer = null;
|
||||
gSysadminHttpServer = null;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ function getTimeZone(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, tz);
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ function exec(tag, file, args, callback) {
|
||||
cp.on('exit', function (code, signal) {
|
||||
if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal);
|
||||
|
||||
callback(code === 0 ? null : new Error(util.format('Exited with error %s signal %s', code, signal)));
|
||||
callback(code === 0 ? null : new Error(util.format(tag + ' exited with error %s signal %s', code, signal)));
|
||||
});
|
||||
|
||||
cp.on('error', function (error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ var apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:src/simpleauth'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
express = require('express'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
@@ -94,6 +95,8 @@ function login(req, res, next) {
|
||||
if (error && error.reason === AppsError.ACCESS_DENIED) return next(new HttpError(401, 'Forbidden'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'simpleauth', clientId: req.body.clientId }, { userId: result.user.id });
|
||||
|
||||
var tmp = {
|
||||
accessToken: result.accessToken,
|
||||
user: {
|
||||
|
||||
+6
-8
@@ -1,19 +1,17 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./sysinfo/caas.js'),
|
||||
config = require('./config.js'),
|
||||
ec2 = require('./sysinfo/ec2.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
SysInfoError: SysInfoError,
|
||||
|
||||
getIp: getIp
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./sysinfo/caas.js'),
|
||||
config = require('./config.js'),
|
||||
ec2 = require('./sysinfo/ec2.js'),
|
||||
util = require('util');
|
||||
|
||||
var gCachedIp = null;
|
||||
|
||||
function SysInfoError(reason, errorOrMessage) {
|
||||
|
||||
@@ -19,11 +19,13 @@ var appdb = require('./appdb.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:taskmanager'),
|
||||
locker = require('./locker.js'),
|
||||
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gActiveTasks = { };
|
||||
var gPendingTasks = [ ];
|
||||
var gPlatformReady = false; // PaaS (addons) up and running
|
||||
|
||||
var TASK_CONCURRENCY = 5;
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
@@ -39,6 +41,11 @@ function initialize(callback) {
|
||||
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
gPlatformReady = true;
|
||||
resumeTasks();
|
||||
}, 30000); // wait 30 seconds to signal platform ready
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -82,6 +89,8 @@ function resumeTasks(callback) {
|
||||
apps.forEach(function (app) {
|
||||
if (app.installationState === appdb.ISTATE_INSTALLED && app.runState === appdb.RSTATE_RUNNING) return;
|
||||
|
||||
if (app.installationState === appdb.ISTATE_ERROR) return;
|
||||
|
||||
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
|
||||
startAppTask(app.id, NOOP_CALLBACK);
|
||||
});
|
||||
@@ -106,6 +115,12 @@ function startAppTask(appId, callback) {
|
||||
return callback(new Error(util.format('Task for %s is already active', appId)));
|
||||
}
|
||||
|
||||
if (!gPlatformReady) {
|
||||
debug('Platform not ready yet, queueing task for %s', appId);
|
||||
gPendingTasks.push(appId);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug('Reached concurrency limit, queueing task for %s', appId);
|
||||
gPendingTasks.push(appId);
|
||||
@@ -130,7 +145,10 @@ function startAppTask(appId, callback) {
|
||||
debug('Task for %s pid %s completed with status %s', appId, pid, code);
|
||||
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
|
||||
debug('Apptask crashed with code %s and signal %s', code, signal);
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
|
||||
} else if (code === 50) {
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
|
||||
+9
-24
@@ -28,7 +28,8 @@ describe('Apps', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var USER_0 = {
|
||||
@@ -40,7 +41,8 @@ describe('Apps', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
@@ -52,7 +54,8 @@ describe('Apps', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var GROUP_0 = 'somegroup';
|
||||
@@ -113,9 +116,9 @@ describe('Apps', function () {
|
||||
groups.create.bind(null, GROUP_1),
|
||||
groups.addMember.bind(null, groups.ADMIN_GROUP_ID, ADMIN_0.id),
|
||||
groups.addMember.bind(null, GROUP_0, USER_1.id),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit)
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, null),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, null)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -201,24 +204,6 @@ describe('Apps', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot getBySubdomain', function (done) {
|
||||
apps.getBySubdomain('moang', function (error, app) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(AppsError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can getBySubdomain', function (done) {
|
||||
apps.getBySubdomain(APP_0.location, function (error, app) {
|
||||
expect(error).to.be(null);
|
||||
expect(app).to.be.ok();
|
||||
expect(app.iconUrl).to.eql(null);
|
||||
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can getAll', function (done) {
|
||||
apps.getAll(function (error, apps) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('apptask', function () {
|
||||
config.set('version', '0.5.0');
|
||||
async.series([
|
||||
database.initialize,
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit),
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit, null),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' })
|
||||
], done);
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
readonly TEST_IMAGE="cloudron/test:10.0.0"
|
||||
readonly TEST_IMAGE="cloudron/test:11.0.0"
|
||||
|
||||
source ${SOURCE_DIR}/setup/INFRA_VERSION
|
||||
source ${SOURCE_DIR}/src/INFRA_VERSION
|
||||
|
||||
# reset sudo timestamp to avoid wrong success
|
||||
sudo -k || sudo --reset-timestamp
|
||||
@@ -13,6 +13,7 @@ sudo -k || sudo --reset-timestamp
|
||||
# checks if all scripts are sudo access
|
||||
scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/createappdir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/setup_infra.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/backupbox.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/backupapp.sh" \
|
||||
|
||||
+64
-10
@@ -7,14 +7,15 @@
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
async = require('async'),
|
||||
authcodedb = require('../authcodedb.js'),
|
||||
backupdb = require('../backupdb.js'),
|
||||
clientdb = require('../clientdb.js'),
|
||||
hat = require('hat'),
|
||||
database = require('../database'),
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
eventlogdb = require('../eventlogdb.js'),
|
||||
expect = require('expect.js'),
|
||||
async = require('async'),
|
||||
hat = require('hat'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
@@ -42,7 +43,8 @@ describe('database', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
@@ -54,7 +56,8 @@ describe('database', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: '',
|
||||
displayName: 'Herbert 1'
|
||||
displayName: 'Herbert 1',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
var USER_2 = {
|
||||
@@ -66,7 +69,8 @@ describe('database', function () {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: '',
|
||||
displayName: 'Herbert 2'
|
||||
displayName: 'Herbert 2',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
it('can add user', function (done) {
|
||||
@@ -498,7 +502,8 @@ describe('database', function () {
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296
|
||||
memoryLimit: 4294967296,
|
||||
altDomain: null
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
@@ -517,7 +522,8 @@ describe('database', function () {
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
@@ -534,7 +540,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -558,7 +564,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add of same app fails', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.memoryLimit, null, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
@@ -630,7 +636,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add second app succeeds', function (done) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_1.memoryLimit, function (error) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_1.memoryLimit, null, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -1069,5 +1075,53 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('eventlog', function () {
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
eventlogdb.add('someid', 'some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
eventlogdb.get('someid', function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.id).to.be('someid');
|
||||
expect(result.action).to.be('some.event');
|
||||
expect(result.creationTime).to.be.a(Date);
|
||||
|
||||
expect(result.source).to.be.eql({ ip: '1.2.3.4' });
|
||||
expect(result.data).to.be.eql({ appId: 'thatapp' });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of unknown id fails', function (done) {
|
||||
eventlogdb.get('notfoundid', function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAllPaged succeeds', function (done) {
|
||||
eventlogdb.getAllPaged(null, null, 1, 1, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.be.an(Array);
|
||||
expect(results.length).to.be(1);
|
||||
|
||||
expect(results[0].id).to.be('someid');
|
||||
expect(results[0].action).to.be('some.event');
|
||||
expect(results[0].source).to.be.eql({ ip: '1.2.3.4' });
|
||||
expect(results[0].data).to.be.eql({ appId: 'thatapp' });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
EventLogError = eventlog.EventLogError;
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('Eventlog', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
var eventId;
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
eventlog.add('some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.id).to.be.ok();
|
||||
|
||||
eventId = result.id;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
eventlog.get(eventId, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.id).to.be(eventId);
|
||||
expect(result.action).to.be('some.event');
|
||||
expect(result.creationTime).to.be.a(Date);
|
||||
|
||||
expect(result.source).to.be.eql({ ip: '1.2.3.4' });
|
||||
expect(result.data).to.be.eql({ appId: 'thatapp' });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of unknown id fails', function (done) {
|
||||
eventlog.get('notfoundid', function (error, result) {
|
||||
expect(error).to.be.a(EventLogError);
|
||||
expect(error.reason).to.be(EventLogError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAllPaged succeeds', function (done) {
|
||||
eventlog.getAllPaged(null, null, 1, 1, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.be.an(Array);
|
||||
expect(results.length).to.be(1);
|
||||
|
||||
expect(results[0].id).to.be(eventId);
|
||||
expect(results[0].action).to.be('some.event');
|
||||
expect(results[0].source).to.be.eql({ ip: '1.2.3.4' });
|
||||
expect(results[0].data).to.be.eql({ appId: 'thatapp' });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,8 @@ var USER_0 = {
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
displayName: '',
|
||||
showTutorial: false
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
|
||||
@@ -34,6 +34,10 @@ var USER_1 = {
|
||||
displayName: 'User 1'
|
||||
};
|
||||
|
||||
var AUDIT_SOURCE = {
|
||||
ip: '1.2.3.4'
|
||||
};
|
||||
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'appStoreId-0',
|
||||
@@ -68,10 +72,10 @@ function setup(done) {
|
||||
database.initialize.bind(null),
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
|
||||
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
|
||||
function (callback) {
|
||||
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, result) {
|
||||
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
USER_0.id = result.id;
|
||||
@@ -80,7 +84,7 @@ function setup(done) {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
user.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, function (error, result) {
|
||||
user.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
USER_1.id = result.id;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
|
||||
rm -rf $HOME/.cloudron_test
|
||||
mkdir -p $HOME/.cloudron_test
|
||||
cd $HOME/.cloudron_test
|
||||
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs
|
||||
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs data/box/mail/dkim/localhost
|
||||
|
||||
webadmin_scopes="root,profile,users,apps,settings"
|
||||
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
|
||||
|
||||
@@ -12,7 +12,6 @@ var appdb = require('../appdb.js'),
|
||||
database = require('../database.js'),
|
||||
deepExtend = require('deep-extend'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
mailer = require('../mailer.js'),
|
||||
nock = require('nock'),
|
||||
settings = require('../settings.js'),
|
||||
@@ -67,6 +66,10 @@ var USER_0 = {
|
||||
displayName: 'User 0'
|
||||
};
|
||||
|
||||
var AUDIT_SOURCE = {
|
||||
ip: '1.2.3.4'
|
||||
};
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
@@ -83,7 +86,7 @@ describe('updatechecker - checkBoxUpdates', function () {
|
||||
async.series([
|
||||
database.initialize,
|
||||
mailer._clearMailQueue,
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName)
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -248,8 +251,8 @@ describe('updatechecker - checkAppUpdates', function () {
|
||||
database.initialize,
|
||||
database._clear,
|
||||
mailer._clearMailQueue,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName)
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
+62
-61
@@ -25,6 +25,8 @@ var NEW_PASSWORD = 'oTHER@#$235';
|
||||
var DISPLAY_NAME = 'Nobody cares';
|
||||
var DISPLAY_NAME_NEW = 'Somone cares';
|
||||
var userObject = null;
|
||||
var NON_ADMIN_GROUP = 'members';
|
||||
var AUDIT_SOURCE = { ip: '1.2.3.4' };
|
||||
|
||||
function cleanupUsers(done) {
|
||||
async.series([
|
||||
@@ -36,13 +38,15 @@ function cleanupUsers(done) {
|
||||
|
||||
function createOwner(done) {
|
||||
groups.create('admin', function () { // ignore error since it might already exist
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
groups.create(NON_ADMIN_GROUP, function () { // ignore error since it might already exist
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
userObject = result;
|
||||
userObject = result;
|
||||
|
||||
done();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -79,7 +83,7 @@ describe('User', function () {
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to short password', function (done) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -89,7 +93,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing upper case password', function (done) {
|
||||
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -99,7 +103,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing numerics in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -109,7 +113,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing special chars in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -119,7 +123,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to reserved username', function (done) {
|
||||
user.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_USERNAME);
|
||||
@@ -129,7 +133,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to reserved username', function (done) {
|
||||
user.create('AdMiN', PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create('AdMiN', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_USERNAME);
|
||||
@@ -139,7 +143,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds and attempts to send invite', function (done) {
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.username).to.equal(USERNAME.toLowerCase());
|
||||
@@ -174,7 +178,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
|
||||
@@ -184,7 +188,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -400,7 +404,7 @@ describe('User', function () {
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unknown userid', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
@@ -409,7 +413,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid email', function (done) {
|
||||
user.update(userObject.id, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, function (error) {
|
||||
user.update(userObject.id, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.BAD_EMAIL);
|
||||
|
||||
@@ -418,7 +422,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
user.get(userObject.id, function (error, result) {
|
||||
@@ -434,7 +438,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds with same data', function (done) {
|
||||
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
|
||||
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
user.get(userObject.id, function (error, result) {
|
||||
@@ -454,32 +458,42 @@ describe('User', function () {
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'ASDFkljsf#$^%2354',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
it('make second user admin succeeds', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'ASDFkljsf#$^%2354',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user1.id = result.id;
|
||||
|
||||
groups.setGroups(user1.id, [ groups.ADMIN_GROUP_ID ], function (error) {
|
||||
user.setGroups(user1.id, [ groups.ADMIN_GROUP_ID ], function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// one mail for user creation, one mail for admin change
|
||||
checkMails(1, done);
|
||||
checkMails(2, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xit('succeeds to remove admin flag of first user', function (done) {
|
||||
groups.setGroups(USERNAME, [], function (error) {
|
||||
it('add user to non admin group does not trigger admin mail', function (done) {
|
||||
user.setGroups(user1.id, [ groups.ADMIN_GROUP_ID, NON_ADMIN_GROUP ], function (error) {
|
||||
expect(error).to.equal(null);
|
||||
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds to remove admin flag', function (done) {
|
||||
user.setGroups(user1.id, [ NON_ADMIN_GROUP ], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
@@ -506,7 +520,7 @@ describe('User', function () {
|
||||
};
|
||||
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -529,53 +543,40 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('password change', function () {
|
||||
describe('set password', function () {
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to wrong arumgent count', function () {
|
||||
expect(function () { user.changePassword(); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD); }).to.throwError();
|
||||
});
|
||||
|
||||
it('fails due to wrong arumgents', function () {
|
||||
expect(function () { user.changePassword(USERNAME, {}, NEW_PASSWORD, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(1337, PASSWORD, NEW_PASSWORD, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, 1337, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, 'some string'); }).to.throwError();
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
user.changePassword(USERNAME, 'wrongpassword', NEW_PASSWORD, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty new password', function (done) {
|
||||
user.changePassword(USERNAME, PASSWORD, '', function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to unknown user', function (done) {
|
||||
user.changePassword('somerandomuser', PASSWORD, NEW_PASSWORD, function (error) {
|
||||
user.setPassword('doesnotexist', NEW_PASSWORD, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
user.setPassword(userObject.id, '', function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid password', function (done) {
|
||||
user.setPassword(userObject.id, 'foobar', function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, function (error) {
|
||||
user.setPassword(userObject.id, NEW_PASSWORD, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('actually changed the password (unable to login with old pasword)', function (done) {
|
||||
user.verifyWithUsername(USERNAME, PASSWORD, function (error, result) {
|
||||
user.verify(userObject.id, PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
@@ -584,7 +585,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('actually changed the password (login with new password)', function (done) {
|
||||
user.verifyWithUsername(USERNAME, NEW_PASSWORD, function (error, result) {
|
||||
user.verify(userObject.id, NEW_PASSWORD, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
done();
|
||||
|
||||
+53
-34
@@ -14,18 +14,19 @@ exports = module.exports = {
|
||||
getAllAdmins: getAllAdmins,
|
||||
resetPasswordByIdentifier: resetPasswordByIdentifier,
|
||||
setPassword: setPassword,
|
||||
changePassword: changePassword,
|
||||
update: updateUser,
|
||||
createOwner: createOwner,
|
||||
getOwner: getOwner,
|
||||
sendInvite: sendInvite,
|
||||
setGroups: setGroups
|
||||
setGroups: setGroups,
|
||||
setShowTutorial: setShowTutorial
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
GroupError = groups.GroupError,
|
||||
hat = require('hat'),
|
||||
@@ -112,11 +113,12 @@ function validateDisplayName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, displayName, options, callback) {
|
||||
function createUser(username, password, email, displayName, auditSource, options, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
@@ -159,13 +161,16 @@ function createUser(username, password, email, displayName, options, callback) {
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
resetToken: hat(256),
|
||||
displayName: displayName
|
||||
displayName: displayName,
|
||||
showTutorial: true
|
||||
};
|
||||
|
||||
userdb.add(user.id, user, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email });
|
||||
|
||||
callback(null, user);
|
||||
|
||||
if (!owner) mailer.userAdded(user, sendInvite);
|
||||
@@ -238,14 +243,17 @@ function verifyWithEmail(email, password, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser(user, callback) {
|
||||
function removeUser(user, auditSource, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.del(user.id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id });
|
||||
|
||||
callback(null);
|
||||
|
||||
mailer.userRemoved(user);
|
||||
@@ -300,11 +308,12 @@ function getByResetToken(resetToken, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser(userId, username, email, displayName, callback) {
|
||||
function updateUser(userId, username, email, displayName, auditSource, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
username = username.toLowerCase();
|
||||
@@ -320,6 +329,9 @@ function updateUser(userId, username, email, displayName, callback) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -329,19 +341,28 @@ function setGroups(userId, groupIds, callback) {
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groups.setGroups(userId, groupIds, function (error) {
|
||||
if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found'));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
groups.getGroups(userId, function (error, oldGroupIds) {
|
||||
if (error && error.reason !== GroupError.NOT_FOUND) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
if (groupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; })) {
|
||||
getUser(userId, function (error, result) {
|
||||
if (error) return console.error('Failed to send admin change mail.', error);
|
||||
oldGroupIds = oldGroupIds || [];
|
||||
|
||||
mailer.adminChanged(result, true);
|
||||
});
|
||||
}
|
||||
groups.setGroups(userId, groupIds, function (error) {
|
||||
if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found'));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
var isAdmin = groupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; });
|
||||
var wasAdmin = oldGroupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; });
|
||||
|
||||
if ((isAdmin && !wasAdmin) || (!isAdmin && wasAdmin)) {
|
||||
getUser(userId, function (error, result) {
|
||||
if (error) return console.error('Failed to send admin change mail.', error);
|
||||
|
||||
mailer.adminChanged(result, isAdmin);
|
||||
});
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -421,27 +442,12 @@ function setPassword(userId, newPassword, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(username, oldPassword, newPassword, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof oldPassword, 'string');
|
||||
assert.strictEqual(typeof newPassword, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
verifyWithUsername(username.toLowerCase(), oldPassword, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
setPassword(user.id, newPassword, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function createOwner(username, password, email, displayName, callback) {
|
||||
function createOwner(username, password, email, displayName, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// This is only not allowed for the owner
|
||||
@@ -451,7 +457,7 @@ function createOwner(username, password, email, displayName, callback) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
|
||||
createUser(username, password, email, displayName, { owner: true }, function (error, user) {
|
||||
createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
groups.addMember(groups.ADMIN_GROUP_ID, user.id, function (error) {
|
||||
@@ -492,3 +498,16 @@ function sendInvite(userId, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setShowTutorial(userId, showTutorial, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof showTutorial, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.update(userId, { showTutorial: showTutorial }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+8
-3
@@ -23,13 +23,14 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
groups = require('./groups.js');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName' ].join(',');
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName', 'showTutorial' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
|
||||
// The username may be null or undefined in the db, let's ensure it is a string
|
||||
result.username = result.username || '';
|
||||
result.showTutorial = !!result.showTutorial;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -137,10 +138,11 @@ function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof user.modifiedAt, 'string');
|
||||
assert.strictEqual(typeof user.resetToken, 'string');
|
||||
assert.strictEqual(typeof user.displayName, 'string');
|
||||
assert.strictEqual(typeof user.showTutorial, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId, user.username || null, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName ];
|
||||
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', data, function (error, result) {
|
||||
var data = [ userId, user.username || null, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName, user.showTutorial ];
|
||||
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName, showTutorial) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -204,6 +206,9 @@ function update(userId, user, callback) {
|
||||
} else if (k === 'email') {
|
||||
assert.strictEqual(typeof user.email, 'string');
|
||||
args.push(user.email);
|
||||
} else if (k === 'showTutorial') {
|
||||
assert.strictEqual(typeof user.showTutorial, 'boolean');
|
||||
args.push(user.showTutorial);
|
||||
} else {
|
||||
args.push(user[k]);
|
||||
}
|
||||
|
||||
+45
-38
@@ -1,43 +1,61 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = waitForDns;
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
attempt = require('attempt'),
|
||||
debug = require('debug')('box:src/waitfordns'),
|
||||
dns = require('native-dns');
|
||||
dns = require('native-dns'),
|
||||
tld = require('tldjs');
|
||||
|
||||
// the first arg to callback is not an error argument; this is required for async.every
|
||||
function isChangeSynced(domain, ip, nameserver, callback) {
|
||||
function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ns records cannot have cname
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) return callback(false);
|
||||
if (error || !nsIps || nsIps.length === 0) {
|
||||
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: domain, type: 'A' }),
|
||||
question: dns.Question({ name: domain, type: type }),
|
||||
server: { address: nsIp },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return iteratorCallback(false); });
|
||||
req.on('timeout', function () {
|
||||
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
|
||||
return iteratorCallback(false);
|
||||
});
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return iteratorCallback(false);
|
||||
if (error) {
|
||||
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
|
||||
return iteratorCallback(false);
|
||||
}
|
||||
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, message.answer[0], ip);
|
||||
var answer = message.answer;
|
||||
|
||||
if (message.answer[0].address !== ip) return iteratorCallback(false);
|
||||
if (!answer || answer.length === 0) {
|
||||
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, message);
|
||||
return iteratorCallback(false);
|
||||
}
|
||||
|
||||
iteratorCallback(true); // done
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
|
||||
|
||||
if ((type === 'A' && answer[0].address === value) ||
|
||||
(type === 'CNAME' && answer[0].data === value)) {
|
||||
return iteratorCallback(true); // done!
|
||||
}
|
||||
|
||||
iteratorCallback(false);
|
||||
});
|
||||
|
||||
req.send();
|
||||
@@ -46,40 +64,29 @@ function isChangeSynced(domain, ip, nameserver, callback) {
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, ip, zoneName, options, callback) {
|
||||
function waitForDns(domain, value, type, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(type === 'A' || type === 'CNAME');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var defaultOptions = {
|
||||
retryInterval: 5000,
|
||||
retries: Infinity
|
||||
};
|
||||
var zoneName = tld.getDomain(domain);
|
||||
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = defaultOptions;
|
||||
} else {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
debug('waitForDNS: domain %s to be %s in zone %s.', domain, ip, zoneName);
|
||||
|
||||
attempt(function (attempts) {
|
||||
var callback = this; // gross
|
||||
debug('waitForDNS: %s attempt %s.', domain, attempts);
|
||||
var attempt = 1;
|
||||
async.retry({ interval: 5000, times: 50000 }, function (retryCallback) {
|
||||
debug('waitForDNS: %s attempt %s.', domain, attempt++);
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
|
||||
if (error || !nameservers) return retryCallback(error || new Error('Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, ip), function (synced) {
|
||||
debug('waitForDNS: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (synced) {
|
||||
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
callback(synced ? null : new Error('ETRYAGAIN'));
|
||||
retryCallback(synced ? null : new Error('ETRYAGAIN'));
|
||||
});
|
||||
});
|
||||
}, { interval: options.retryInterval, retries: options.retries }, function (error) {
|
||||
}, function retryDone(error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('waitForDNS: %s done.', domain);
|
||||
|
||||
@@ -58,6 +58,35 @@
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Welcome logic -->
|
||||
<div class="welcome" ng-show="tutorialSteps[tutorialStep]">
|
||||
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'intro'">
|
||||
<h2>Welcome {{ user.username }}</h2>
|
||||
<p>This is the administration page of your Cloudron. It shows your installed apps.</p>
|
||||
<br/>
|
||||
<button class="btn btn-default pull-left" ng-click="endTutorial();">Skip</button>
|
||||
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Next: <b>Cloudron Store</b></button>
|
||||
</div>
|
||||
|
||||
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'appstore'">
|
||||
<h2>Cloudron Store</h2>
|
||||
<p>Install apps easily into any subdomain and control who can access it.</p>
|
||||
<p>Your apps stay updated, backed up and secure with no effort on your part.</p>
|
||||
<br/>
|
||||
<button class="btn btn-default pull-left" ng-click="prevTutorialStep();">Back</button>
|
||||
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Last: <b>Users and Groups</b></button>
|
||||
</div>
|
||||
|
||||
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'users'">
|
||||
<h2>Users and Groups</h2>
|
||||
<p>The Cloudron comes with built-in user and group managment</p>
|
||||
<p>Remember, you can use the same credentials to login to all your apps.</p>
|
||||
<br/>
|
||||
<button class="btn btn-default pull-left" ng-click="prevTutorialStep();">Back</button>
|
||||
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Start using apps and invite users</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal update -->
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
@@ -111,6 +140,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal plan upgrade -->
|
||||
<div class="modal fade" id="upgradeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Cloudron Upgrade</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>See available plans for upgrade at <a href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">cloudron.io</a>.</p>
|
||||
<p>If you request an upgrade, we will get back to you as soon as possible.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-click="requestUpgrade()" ng-disabled="upgradeRequest.busy"><i class="fa fa-spinner fa-pulse" ng-show="upgradeRequest.busy"></i> Request Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide" ng-show="initialized">
|
||||
|
||||
<!-- Navigation -->
|
||||
@@ -149,6 +198,7 @@
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
|
||||
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
|
||||
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
|
||||
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
|
||||
@@ -164,6 +164,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._userInfo.email = userInfo.email;
|
||||
this._userInfo.displayName = userInfo.displayName;
|
||||
this._userInfo.admin = !!userInfo.admin;
|
||||
this._userInfo.showTutorial = !!userInfo.showTutorial;
|
||||
this._userInfo.gravatar = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=24&d=mm';
|
||||
this._userInfo.gravatarHuge = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=128&d=mm';
|
||||
};
|
||||
@@ -294,7 +295,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
accessRestriction: config.accessRestriction,
|
||||
cert: config.cert,
|
||||
key: config.key,
|
||||
memoryLimit: config.memoryLimit
|
||||
memoryLimit: config.memoryLimit,
|
||||
altDomain: config.altDomain || null
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
|
||||
@@ -378,6 +380,22 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getEventLogs = function (action, search, page, perPage, callback) {
|
||||
var config = {
|
||||
params: {
|
||||
action: action,
|
||||
search: search,
|
||||
page: page,
|
||||
per_page: perPage
|
||||
}
|
||||
};
|
||||
$http.get(client.apiOrigin + '/api/v1/eventlog', config).success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.eventlogs);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getApps = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/apps').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
@@ -498,13 +516,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.listUsers = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/users').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getOAuthClients = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/oauth/clients').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
@@ -625,7 +636,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
newPassword: newPassword
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/users/' + this._userInfo.id + '/password', data).success(function(data, status) {
|
||||
$http.put(client.apiOrigin + '/api/v1/profile/password', data).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
@@ -768,6 +779,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
return (available - needed) >= 0;
|
||||
};
|
||||
|
||||
Client.prototype.setShowTutorial = function (show, callback) {
|
||||
var data = { showTutorial: show };
|
||||
|
||||
$http.put(client.apiOrigin + '/api/v1/profile/tutorial', data).success(function (data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
}
|
||||
|
||||
client = new Client();
|
||||
return client;
|
||||
}]);
|
||||
|
||||
@@ -48,6 +48,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html'
|
||||
}).when('/activity', {
|
||||
controller: 'ActivityController',
|
||||
templateUrl: 'views/activity.html'
|
||||
}).when('/support', {
|
||||
controller: 'SupportController',
|
||||
templateUrl: 'views/support.html'
|
||||
@@ -207,6 +210,53 @@ app.filter('markdown2html', function () {
|
||||
};
|
||||
});
|
||||
|
||||
// keep this in sync with eventlog.js
|
||||
var ACTION_ACTIVATE = 'cloudron.activate';
|
||||
var ACTION_APP_CONFIGURE = 'app.configure';
|
||||
var ACTION_APP_INSTALL = 'app.install';
|
||||
var ACTION_APP_RESTORE = 'app.restore';
|
||||
var ACTION_APP_UNINSTALL = 'app.uninstall';
|
||||
var ACTION_APP_UPDATE = 'app.update';
|
||||
var ACTION_APP_UPDATE = 'app.update';
|
||||
var ACTION_BACKUP_FINISH = 'backup.finish';
|
||||
var ACTION_BACKUP_START = 'backup.start';
|
||||
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
|
||||
var ACTION_CLI_MODE = 'settings.climode';
|
||||
var ACTION_START = 'cloudron.start';
|
||||
var ACTION_UPDATE = 'cloudron.update';
|
||||
var ACTION_USER_ADD = 'user.add';
|
||||
var ACTION_USER_LOGIN = 'user.login';
|
||||
var ACTION_USER_REMOVE = 'user.remove';
|
||||
var ACTION_USER_UPDATE = 'user.update';
|
||||
|
||||
app.filter('eventLogDetails', function() {
|
||||
return function(eventLog) {
|
||||
var source = eventLog.source;
|
||||
var data = eventLog.data;
|
||||
var errorMessage = data.errorMessage;
|
||||
|
||||
switch (eventLog.action) {
|
||||
case ACTION_ACTIVATE: return 'Cloudron activated';
|
||||
case ACTION_APP_CONFIGURE: return 'App ' + data.appId + ' was configured';
|
||||
case ACTION_APP_INSTALL: return 'App ' + data.manifest.id + '@' + data.manifest.version + ' installed at ' + data.location + ' with id ' + data.appId;
|
||||
case ACTION_APP_RESTORE: return 'App ' + data.appId + ' restored';
|
||||
case ACTION_APP_UNINSTALL: return 'App ' + data.appId + ' uninstalled';
|
||||
case ACTION_APP_UPDATE: return 'App ' + data.appId + ' updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
|
||||
case ACTION_BACKUP_START: return 'Backup started';
|
||||
case ACTION_BACKUP_FINISH: return 'Backup finished. ' + (errorMessage ? ('error: ' + errorMessage) : ('id: ' + data.filename));
|
||||
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : 'succeeded');
|
||||
case ACTION_CLI_MODE: return 'CLI mode was ' + (data.enabled ? 'enabled' : 'disabled');
|
||||
case ACTION_START: return 'Cloudron started with version ' + data.version;
|
||||
case ACTION_UPDATE: return 'Updating to version ' + data.boxUpdateInfo.version;
|
||||
case ACTION_USER_ADD: return 'User ' + data.email + ' added with id ' + data.userId;
|
||||
case ACTION_USER_LOGIN: return 'User ' + data.userId + ' logged in';
|
||||
case ACTION_USER_REMOVE: return 'User ' + data.userId + ' removed';
|
||||
case ACTION_USER_UPDATE: return 'User ' + data.userId + ' updated';
|
||||
default: return eventLog.action;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// custom directive for dynamic names in forms
|
||||
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
|
||||
app.directive('laterName', function () { // (2)
|
||||
|
||||
+61
-1
@@ -7,12 +7,52 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
|
||||
$scope.tutorialStep = -1;
|
||||
$scope.tutorialSteps = [
|
||||
{ title: 'intro', page: '#/apps' },
|
||||
{ title: 'appstore', page: '#/appstore' },
|
||||
{ title: 'users', page: '#/users' }
|
||||
];
|
||||
|
||||
$scope.startTutorial = function () {
|
||||
$scope.tutorialStep = 0;
|
||||
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
|
||||
};
|
||||
|
||||
$scope.endTutorial = function () {
|
||||
$scope.tutorialStep = -1;
|
||||
|
||||
Client.setShowTutorial(false, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
window.location.href = '#/apps';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nextTutorialStep = function () {
|
||||
$scope.tutorialStep += 1;
|
||||
|
||||
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
|
||||
|
||||
if ($scope.tutorialStep >= $scope.tutorialSteps.length) $scope.endTutorial();
|
||||
};
|
||||
|
||||
$scope.prevTutorialStep = function () {
|
||||
$scope.tutorialStep -= 1;
|
||||
|
||||
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
|
||||
};
|
||||
|
||||
$scope.update = {
|
||||
busy: false,
|
||||
error: {},
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.upgradeRequest = {
|
||||
busy: false
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
@@ -37,6 +77,23 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
window.location.href = '/error.html';
|
||||
};
|
||||
|
||||
$scope.requestUpgrade = function () {
|
||||
$scope.upgradeRequest.busy = true;
|
||||
|
||||
var subject = 'User requested upgrade for ' + $scope.config.fqdn;
|
||||
var description = 'User ' + $scope.user.email + ' requested an upgrade for ' + $scope.config.fqdn + '. Get back to him!!';
|
||||
|
||||
Client.feedback('upgrade_request', subject, description, function (error) {
|
||||
$scope.upgradeRequest.busy = false;
|
||||
|
||||
if (error) return Client.notify('Error', error.message, false, 'error');
|
||||
|
||||
Client.notify('Success', 'We will get back to you as soon as possible for the upgrade.', true, 'success');
|
||||
|
||||
$('#upgradeModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showUpdateModal = function (form) {
|
||||
$scope.update.error.password = null;
|
||||
$scope.update.password = '';
|
||||
@@ -85,7 +142,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
}
|
||||
|
||||
|
||||
Client.refreshUserInfo(function (error, result) {
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
@@ -121,6 +178,9 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// welcome screen
|
||||
if ($scope.user.showTutorial && $scope.user.admin) $scope.startTutorial();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -997,3 +997,50 @@ $graphs-success-alt: lighten(#27CE65, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// Welcome tutorial
|
||||
// ----------------------------
|
||||
.welcome {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
z-index: 3000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.dialog {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
box-shadow: 0 3px 5px rgba(0,0,0,.5);
|
||||
right: 20px;
|
||||
bottom: 80px;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: inline-block;
|
||||
padding-left: 0;
|
||||
margin: 20px 0;
|
||||
border-radius: 2px;
|
||||
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-xs btn-default" ng-click="showTutorial()">Show Tutorial</button>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="passwordchange.show()">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -173,6 +173,13 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showTutorial = function () {
|
||||
Client.setShowTutorial(true, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.$parent.startTutorial();
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.tokenInUse = Client._token;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h1>Activity Log</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="filter">
|
||||
<input type="text" class="form-control" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="Search"/>
|
||||
<select class="form-control" ng-model="action" ng-options="a.name for a in actions" ng-change="updateFilter()">
|
||||
<option value="">-- all actions --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="busy || pageItems > eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<center ng-show="busy"><h2><i class="fa fa-spinner fa-pulse"></i></h2></center>
|
||||
<table ng-hide="busy" class="table table-striped table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-2">Time</th>
|
||||
<th class="col-md-2">Source</th>
|
||||
<th class="col-md-6">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="eventLog in eventLogs">
|
||||
<th scope="row">{{ eventLog.creationTime | prettyDate }}</td>
|
||||
<td>{{ eventLog.source.username || eventLog.source.userId || eventLog.source.authType }} <span ng-show="eventLog.source.ip || eventLog.source.appId"> ({{ eventLog.source.ip || eventLog.source.appId }}) </span> </td>
|
||||
<td>{{ eventLog | eventLogDetails }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.eventLogs = [ ];
|
||||
|
||||
// TODO sync this with the eventlog filter
|
||||
$scope.actions = [
|
||||
{ name: 'cloudron.activate', value: 'cloudron.activate' },
|
||||
{ name: 'app.configure', value: 'app.configure' },
|
||||
{ name: 'app.install', value: 'app.install' },
|
||||
{ name: 'app.restore', value: 'app.restore' },
|
||||
{ name: 'app.uninstall', value: 'app.uninstall' },
|
||||
{ name: 'app.update', value: 'app.update' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
{ name: 'backup.start', value: 'backup.start' },
|
||||
{ name: 'certificate.renew', value: 'certificate.renew' },
|
||||
{ name: 'settings.climode', value: 'settings.climode' },
|
||||
{ name: 'cloudron.start', value: 'cloudron.start' },
|
||||
{ name: 'cloudron.update', value: 'cloudron.update' },
|
||||
{ name: 'user.add', value: 'user.add' },
|
||||
{ name: 'user.login', value: 'user.login' },
|
||||
{ name: 'user.remove', value: 'user.remove' },
|
||||
{ name: 'user.update', value: 'user.update' }
|
||||
];
|
||||
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = 20;
|
||||
$scope.action = '';
|
||||
$scope.search = '';
|
||||
|
||||
function fetchEventLogs() {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.getEventLogs($scope.action ? $scope.action.value : null, $scope.search || null, $scope.currentPage, $scope.pageItems, function (error, eventLogs) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.eventLogs = eventLogs;
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchEventLogs();
|
||||
});
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.updateFilter = function () {
|
||||
fetchEventLogs();
|
||||
};
|
||||
}]);
|
||||
@@ -30,12 +30,30 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
||||
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<div class="input-group-addon">
|
||||
{{ !appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
||||
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="{{ appConfigure.usingAltDomain ? 'app.example.com' : 'Leave empty to use bare domain' }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + config.fqdn) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(false)">{{ config.fqdn }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(true)"><i class="fa fa-star"></i> External Domain</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainValid()">
|
||||
Add a CNAME record for {{ appConfigure.location }} to {{ appConfigure.app.fqdn }}
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.port">{{ appConfigure.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
@@ -106,7 +124,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
|
||||
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
|
||||
@@ -123,7 +141,7 @@
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid()) || !appConfigure.isAltDomainValid()"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +152,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really Restore {{ appRestore.app.location }}</h4>
|
||||
<h4 class="modal-title">Really restore {{ appRestore.app.fqdn }} ?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-show="appRestore.app.lastBackupId !== null">Restoring the app will lose all content generated since last backup of this app!</p>
|
||||
@@ -167,7 +185,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ appError.app.location }}</h4>
|
||||
<h4 class="modal-title">{{ appError.app.fqdn }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><b>There was an error:</b></p>
|
||||
@@ -185,7 +203,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really uninstall {{ appUninstall.app.location }}</h4>
|
||||
<h4 class="modal-title">Really uninstall {{ appUninstall.app.fqdn }} ?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Deleting the app will also remove all content generated within this app!</p>
|
||||
@@ -217,7 +235,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Update {{ appUpdate.app.location }}</h4>
|
||||
<h4 class="modal-title">Update {{ appUpdate.app.fqdn }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
|
||||
@@ -297,7 +315,7 @@
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title">{{ app.location || app.fqdn }}</div>
|
||||
<div class="grid-item-top-title">{{ app.altDomain || app.location || app.fqdn }}</div>
|
||||
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
error: {},
|
||||
app: {},
|
||||
location: '',
|
||||
usingAltDomain: false,
|
||||
password: '',
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
@@ -38,6 +39,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appConfigure.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
|
||||
isAltDomainValid: function () {
|
||||
if (!$scope.appConfigure.usingAltDomain) return true;
|
||||
return /.+\..+\..+/.test($scope.appConfigure.location); // 2 dots
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,6 +79,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.error = {};
|
||||
$scope.appConfigure.app = {};
|
||||
$scope.appConfigure.location = '';
|
||||
$scope.appConfigure.usingAltDomain = false;
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
@@ -150,12 +157,23 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
else groups.splice(pos, 1);
|
||||
};
|
||||
|
||||
$scope.useAltDomain = function (use) {
|
||||
$scope.appConfigure.usingAltDomain = use;
|
||||
|
||||
if (use) {
|
||||
$scope.appConfigure.location = '';
|
||||
} else {
|
||||
$scope.appConfigure.location = $scope.appConfigure.app.location;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.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 ? 'restricted' : '';
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
|
||||
@@ -190,7 +208,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.location || '',
|
||||
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 ? null : $scope.appConfigure.accessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
|
||||
@@ -103,10 +103,11 @@
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">This Cloudron is running low on resources.</h4>
|
||||
<p>Installing this app might decrease the performance of other apps. The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
|
||||
<p>Installing this app might decrease the performance of other apps. The Cloudron's resources can be extended with a plan upgrade or available resources may be freed up by uninstalling unused applications.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success pull-left" ng-show="!appInstall.installFormVisible && user.admin && appInstall.resourceConstraintVisible" ng-click="showRequestUpgrade()">Upgrade Cloudron</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="!appInstall.installFormVisible && user.admin && appInstall.resourceConstraintVisible" ng-click="appInstall.showForm(true)">Install anyway</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="appInstall.showForm()">Install</button>
|
||||
|
||||
@@ -60,8 +60,10 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
if ($scope.appInstallForm) {
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
}
|
||||
},
|
||||
|
||||
showForm: function (force) {
|
||||
@@ -314,6 +316,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('#appNotFoundModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showRequestUpgrade = function () {
|
||||
$('#appInstallModal').modal('hide');
|
||||
$('#upgradeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.gotoApp = function (app) {
|
||||
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
||||
};
|
||||
|
||||
@@ -107,6 +107,10 @@
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<button class="btn btn-primary pull-right" data-toggle="modal" data-target="#upgradeModal">Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -331,10 +331,10 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
$scope.groups = result;
|
||||
|
||||
Client.listUsers(function (error, result) {
|
||||
Client.getUsers(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
$scope.users = result.users;
|
||||
$scope.users = result;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user