Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -477,3 +477,24 @@
|
||||
[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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
+11
-1
@@ -65,6 +65,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 +100,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 +108,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
+692
-508
File diff suppressed because it is too large
Load Diff
+8
-9
@@ -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,16 +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",
|
||||
"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",
|
||||
@@ -59,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+5
-4
@@ -59,7 +59,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(',');
|
||||
|
||||
@@ -177,7 +177,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 +186,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 +196,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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+39
-13
@@ -50,6 +50,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'),
|
||||
@@ -261,7 +262,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);
|
||||
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
@@ -276,7 +277,7 @@ function getBySubdomain(subdomain, 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);
|
||||
});
|
||||
@@ -294,7 +295,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 +310,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 +354,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 +365,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 +390,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 +412,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 +424,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 +439,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 +452,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 +478,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 +498,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(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 +560,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 +572,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 +624,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 +657,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 +669,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 +688,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);
|
||||
});
|
||||
});
|
||||
|
||||
+32
-17
@@ -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) {
|
||||
@@ -332,6 +332,12 @@ function waitForDnsPropagation(app, callback) {
|
||||
}, callback);
|
||||
}
|
||||
|
||||
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
|
||||
function updateApp(app, values, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
@@ -408,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),
|
||||
|
||||
@@ -509,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),
|
||||
|
||||
@@ -571,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),
|
||||
|
||||
|
||||
+20
-10
@@ -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);
|
||||
// });
|
||||
// }
|
||||
@@ -403,19 +409,22 @@ function backupBoxAndApps(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backup(callback) {
|
||||
function backup(eventSource, callback) {
|
||||
assert.strictEqual(typeof eventSource, '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');
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, eventSource, { });
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
|
||||
|
||||
backupBoxAndApps(function (error, filename) { // start the backup operation in the background
|
||||
if (error) console.error('backup failed.', error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, eventSource, { errorMessage: error ? error.message : null, filename: filename });
|
||||
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
});
|
||||
|
||||
@@ -436,7 +445,8 @@ function ensureBackup(callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(callback);
|
||||
var eventSource = { userId: null, username: 'cron' };
|
||||
backup(eventSource, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+25
-18
@@ -9,6 +9,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,7 +18,6 @@ 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'),
|
||||
@@ -57,11 +57,14 @@ util.inherits(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
|
||||
function getApi(callback) {
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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 +92,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 +128,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 +146,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);
|
||||
@@ -257,7 +262,7 @@ function setAdminCertificate(cert, key, callback) {
|
||||
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 +276,12 @@ function setAdminCertificate(cert, key, callback) {
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
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 +294,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;
|
||||
}
|
||||
|
||||
+22
-9
@@ -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'),
|
||||
@@ -102,7 +103,7 @@ function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
exports.events.on(exports.EVENT_FIRST_RUN, function () { setTimeout(installAppBundle, 30000); }); // wait for 30sec for the addons to start up
|
||||
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
|
||||
|
||||
// check activation state for existing cloudrons that do not have first run file
|
||||
// can be removed once cloudrons have been updated
|
||||
@@ -190,36 +191,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 +247,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 });
|
||||
});
|
||||
});
|
||||
@@ -510,12 +520,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);
|
||||
}
|
||||
|
||||
@@ -641,7 +654,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);
|
||||
|
||||
+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 });
|
||||
});
|
||||
}
|
||||
|
||||
+3
-2
@@ -138,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
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
'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(page, perPage, callback) {
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getAllPaged(page, perPage, function (error, boxes) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'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'),
|
||||
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(page, perPage, callback) {
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog ORDER BY creationTime DESC LIMIT ?,?', [ (page-1)*perPage, perPage ], 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
@@ -177,6 +178,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.
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ exports = module.exports = {
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
sendCrashNotification: sendCrashNotification,
|
||||
unexpectedExit: unexpectedExit,
|
||||
|
||||
appDied: appDied,
|
||||
|
||||
@@ -395,7 +395,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');
|
||||
|
||||
@@ -403,7 +403,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 ]);
|
||||
|
||||
+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
-8
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -34,6 +32,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 +52,8 @@ function removeInternalAppFields(app) {
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
fqdn: app.fqdn,
|
||||
memoryLimit: app.memoryLimit
|
||||
memoryLimit: app.memoryLimit,
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,13 +129,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 +173,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 +196,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 +230,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 +280,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 || null, 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 = {
|
||||
|
||||
+10
-7
@@ -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));
|
||||
|
||||
@@ -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,23 @@
|
||||
'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'));
|
||||
|
||||
eventlog.getAllPaged(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 = {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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'),
|
||||
oauth2: require('./oauth2.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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -16,6 +14,11 @@ var assert = require('assert'),
|
||||
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');
|
||||
|
||||
@@ -49,7 +52,7 @@ function update(req, res, next) {
|
||||
|
||||
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, function (error) {
|
||||
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'));
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,119 @@
|
||||
/* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -155,7 +155,8 @@ describe('OAuth2', function () {
|
||||
location: 'test',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -165,7 +166,8 @@ describe('OAuth2', function () {
|
||||
location: 'test1',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -175,7 +177,8 @@ describe('OAuth2', function () {
|
||||
location: 'test2',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ USER_0.id ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
var APP_3 = {
|
||||
@@ -185,7 +188,8 @@ describe('OAuth2', function () {
|
||||
location: 'test3',
|
||||
portBindings: {},
|
||||
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
// unknown app
|
||||
@@ -308,12 +312,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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,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) {
|
||||
|
||||
+8
-5
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -23,6 +21,11 @@ var assert = require('assert'),
|
||||
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 create(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
@@ -37,7 +40,7 @@ function create(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'));
|
||||
@@ -73,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'));
|
||||
@@ -130,7 +133,7 @@ function remove(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));
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+15
-8
@@ -9,10 +9,12 @@ var 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'),
|
||||
@@ -65,12 +67,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;
|
||||
@@ -174,11 +176,15 @@ function initializeExpressSync() {
|
||||
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);
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.settings.getTimeZone);
|
||||
|
||||
// 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/: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);
|
||||
@@ -256,7 +262,8 @@ function start(callback) {
|
||||
mailer.initialize,
|
||||
cron.initialize,
|
||||
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
|
||||
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '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);
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -113,9 +113,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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
@@ -498,7 +499,8 @@ describe('database', function () {
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296
|
||||
memoryLimit: 4294967296,
|
||||
altDomain: null
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
@@ -517,7 +519,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 +537,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 +561,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 +633,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 +1072,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(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(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+18
-16
@@ -26,6 +26,8 @@ var DISPLAY_NAME = 'Nobody cares';
|
||||
var DISPLAY_NAME_NEW = 'Somone cares';
|
||||
var userObject = null;
|
||||
|
||||
var AUDIT_SOURCE = { ip: '1.2.3.4' };
|
||||
|
||||
function cleanupUsers(done) {
|
||||
async.series([
|
||||
groupdb._clear,
|
||||
@@ -36,7 +38,7 @@ 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) {
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -79,7 +81,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 +91,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 +101,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 +111,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 +121,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 +131,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 +141,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 +176,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 +186,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 +402,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 +411,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 +420,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 +436,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) {
|
||||
@@ -462,7 +464,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.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -506,7 +508,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();
|
||||
|
||||
|
||||
+17
-5
@@ -25,6 +25,7 @@ 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'),
|
||||
@@ -111,11 +112,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;
|
||||
@@ -165,6 +167,8 @@ function createUser(username, password, email, displayName, options, callback) {
|
||||
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);
|
||||
@@ -237,14 +241,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);
|
||||
@@ -299,11 +306,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();
|
||||
@@ -319,6 +327,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);
|
||||
});
|
||||
}
|
||||
@@ -420,11 +431,12 @@ function setPassword(userId, 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
|
||||
@@ -434,7 +446,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) {
|
||||
|
||||
+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);
|
||||
|
||||
@@ -169,6 +169,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>
|
||||
|
||||
@@ -294,7 +294,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 +379,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getEventLogs = function (page, perPage, callback) {
|
||||
var config = {
|
||||
params: {
|
||||
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));
|
||||
@@ -618,7 +633,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
newPassword: newPassword
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/profile/' + 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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-1">
|
||||
<h1>Activity Log</h1>
|
||||
</div>
|
||||
|
||||
<div class="pagination col-md-offset-1">
|
||||
<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>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<table 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,39 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.eventLogs = [ ];
|
||||
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = 20;
|
||||
|
||||
|
||||
function fetchEventLogs() {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.getEventLogs($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();
|
||||
};
|
||||
}]);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user