Compare commits

...

59 Commits

Author SHA1 Message Date
Girish Ramakrishnan 9266302c4c Print graphite container id 2015-08-13 15:57:36 -07:00
Girish Ramakrishnan 755dce7bc4 fix graph issue finally 2015-08-13 15:54:27 -07:00
Girish Ramakrishnan dd3e38ae55 Use latest graphite 2015-08-13 15:53:36 -07:00
Girish Ramakrishnan 9dfaa2d20f Create symlink in start.sh (and not container setup) 2015-08-13 15:36:21 -07:00
Girish Ramakrishnan d6a4ff23e2 restart mysql in start.sh and not container setup 2015-08-13 15:16:01 -07:00
Girish Ramakrishnan c2ab7e2c1f restart collectd 2015-08-13 15:04:57 -07:00
Girish Ramakrishnan b9e4662dbb fix graphs again 2015-08-13 15:03:44 -07:00
Girish Ramakrishnan 10df0a527f Fix typo
remove thead_cache_size. it's dynamic anyways
2015-08-13 14:53:05 -07:00
Girish Ramakrishnan 9aad3688e1 Revert "Add hack to make graphs work with latest collectd"
This reverts commit a959418544.
2015-08-13 14:42:47 -07:00
Girish Ramakrishnan e78dbcb5d4 limit threads and max connections 2015-08-13 14:42:36 -07:00
Girish Ramakrishnan 5e8cd09f51 Bump infra version 2015-08-13 14:22:39 -07:00
Girish Ramakrishnan 22f65a9364 Add hack to make graphs work with latest collectd
For some reason df-vda1 is not being collected by carbon. I have tried
all sorts of things and nothing works. This is a hack to get it working.
2015-08-13 13:47:44 -07:00
Girish Ramakrishnan 81b7432044 Turn off performance_schema in mysql 5.6 2015-08-13 13:47:44 -07:00
Girish Ramakrishnan d49b90d9f2 Remove unused nodejs-disks 2015-08-13 10:34:06 -07:00
Girish Ramakrishnan 9face9cf35 systemd has moved around the cgroup hierarchy
https://github.com/docker/docker/issues/9902

There is some rationale here:
https://libvirt.org/cgroups.html
2015-08-13 10:21:33 -07:00
Girish Ramakrishnan 33ac34296e CpuShares is part of HostConfig 2015-08-12 23:47:35 -07:00
Girish Ramakrishnan 670ffcd489 Add warning 2015-08-12 19:52:23 -07:00
Girish Ramakrishnan ec7b365c31 Use BASE_IMAGE as well 2015-08-12 19:51:44 -07:00
Girish Ramakrishnan 433d78c7ff Fix graphite version 2015-08-12 19:51:08 -07:00
Girish Ramakrishnan ed041fdca6 Put image names in one place 2015-08-12 19:38:44 -07:00
Girish Ramakrishnan b8e4ed2369 Use latest images 2015-08-12 19:19:58 -07:00
Johannes Zellner d12f260d12 Prevent accessing oldConfig if it does not exist 2015-08-12 21:17:52 +02:00
Johannes Zellner ba7989b57b Add ldap 'users' group 2015-08-12 17:38:31 +02:00
Johannes Zellner 88df410f5b Add ldap search unit tests 2015-08-12 15:31:54 +02:00
Johannes Zellner 2436db3b1f Add ldap memberof attribute 2015-08-12 15:31:44 +02:00
Johannes Zellner d15874df63 Add initial ldap unit tests 2015-08-12 15:00:38 +02:00
Johannes Zellner 8fb90254cd Ensure the focus is properly set when restoring 2015-08-12 14:35:51 +02:00
Johannes Zellner cbd712c20e Better integrate the progress bar 2015-08-12 14:32:20 +02:00
Johannes Zellner 8c004798f2 Improve login form layout 2015-08-12 14:23:13 +02:00
Johannes Zellner c1b0cbe78d Give appstore hover a different color 2015-08-12 14:07:40 +02:00
Johannes Zellner 5ee72c8e98 Make webadmin pages a bit more streamlined with padding 2015-08-12 13:48:55 +02:00
Girish Ramakrishnan c125cc17dc Apps must only get 50% less cpu than system processes when there is a contention for cpu 2015-08-11 17:00:48 -07:00
Johannes Zellner 18feff1bfb Increase installed app title 2015-08-11 15:22:30 +02:00
Johannes Zellner f74f713bbd Hide geeky toolbar in apps icons 2015-08-11 13:04:50 +02:00
Girish Ramakrishnan 0ea14db172 Fix redis installation on 1.7 2015-08-10 23:00:24 -07:00
Girish Ramakrishnan 74785a40d5 r -> ro (docker 1.7) 2015-08-10 21:14:28 -07:00
Girish Ramakrishnan dcfcd5be84 Create docker volume directories since docker 1.7 does not create them 2015-08-10 21:00:56 -07:00
Girish Ramakrishnan 814674eac5 addons can be null in apps.backupApp
addons.backup already takes care of null.

a future commit will give defaults for all non-default manifest fields
at some point and document them as so
2015-08-10 13:47:51 -07:00
Girish Ramakrishnan 1a7fff9867 Keep linter happy 2015-08-10 13:42:04 -07:00
Johannes Zellner 30b248a0f6 Allow non published versions to be shown if explicitly requested
Fixes #468
2015-08-10 16:16:40 +02:00
Johannes Zellner 7168455de3 Do not use table layout for login view
Fixes #458
2015-08-10 15:26:45 +02:00
Johannes Zellner 085f63e3c7 Show cloudron name in login screen 2015-08-10 15:04:12 +02:00
Johannes Zellner 015be64923 Show cloudron avatar in login screen 2015-08-10 15:01:58 +02:00
Johannes Zellner 2c2471811d Restructure the login page 2015-08-10 14:51:04 +02:00
Johannes Zellner 1025249e93 Since addons are optional, ensure we have a valid empty object in the db 2015-08-10 10:37:55 +02:00
Johannes Zellner 41ffc4bcf3 If we have an empty app search show modal dialog link 2015-08-09 15:19:21 +02:00
Johannes Zellner 2739d54cc1 Make appstore feedback form a modal dialog 2015-08-09 14:48:00 +02:00
Girish Ramakrishnan c4c463cbc2 collect logs using a sudo script
docker logs can only be read by root
2015-08-08 19:04:59 -07:00
Girish Ramakrishnan 8cd13bd43f Update safetydance 2015-08-08 18:53:16 -07:00
Girish Ramakrishnan e4ef279759 Update safetydance and lastmile 2015-08-06 13:54:15 -07:00
Girish Ramakrishnan cf7fecb57b bump cloudron-manifestformat 2015-08-06 13:50:27 -07:00
Girish Ramakrishnan 226041dcb1 Display settings path
Fixes #465
2015-08-06 13:44:09 -07:00
Johannes Zellner 7548025561 If an app search is empty, show hint to give feedback 2015-08-06 18:35:08 +02:00
Johannes Zellner fdbee427ee Show app feedback form in appstore
Fixes #461
2015-08-06 18:30:49 +02:00
Johannes Zellner d861d6d6e4 Properly offset the footer in support view 2015-08-06 18:30:25 +02:00
Johannes Zellner 0a648edcaa Add app feedback category 2015-08-06 17:34:40 +02:00
Johannes Zellner 18850c1fba Cloudron prices are in cents 2015-08-06 16:24:19 +02:00
Girish Ramakrishnan f6df4cab67 Remove ADMIN_ORIGIN 2015-08-05 17:27:55 -07:00
Johannes Zellner 019d29c5b7 Use assert.strictEqual() to see the values 2015-08-05 17:49:19 +02:00
39 changed files with 867 additions and 264 deletions
+8 -38
View File
@@ -7,53 +7,23 @@
// !! No console.log() allowed // !! No console.log() allowed
// !! Do not set DEBUG // !! Do not set DEBUG
var supervisor = require('supervisord-eventlistener'), var assert = require('assert'),
mailer = require('./src/mailer.js'),
safe = require('safetydance'), safe = require('safetydance'),
assert = require('assert'), supervisor = require('supervisord-eventlistener'),
exec = require('child_process').exec, path = require('path'),
util = require('util'), util = require('util');
mailer = require('./src/mailer.js');
var gLastNotifyTime = {}; var gLastNotifyTime = {};
var gCooldownTime = 1000 * 60 * 5; // 5 min var gCooldownTime = 1000 * 60 * 5; // 5 min
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
function collectLogs(program, callback) { function collectLogs(program, callback) {
assert.strictEqual(typeof program, 'string'); assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var logFilePath = util.format('/var/log/supervisor/%s.log', program); var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
var boxLogData = safe.fs.readFileSync(logFilePath, 'utf-8');
if (boxLogData === null) return callback(safe.error);
var boxLogLines = boxLogData.split('\n').slice(-100);
var dockerLogPath = '/var/log/upstart/docker.log';
var dockerLogData = safe.fs.readFileSync(dockerLogPath, 'utf-8');
if (dockerLogData === null) return callback(safe.error);
var dockerLogLines = dockerLogData.split('\n').slice(-100);
exec('dmesg', function (error, stdout /*, stderr */) {
if (error) console.error(error);
var lines = stdout.split('\n');
var dmesgLogLines = lines.slice(-100);
var result = '';
result += program + '.log\n';
result += '-------------------------------------\n';
result += boxLogLines.join('\n');
result += '\n\n';
result += 'dmesg\n';
result += '-------------------------------------\n';
result += dmesgLogLines.join('\n');
result += '\n\n';
result += 'docker\n';
result += '-------------------------------------\n';
result += dockerLogLines.join('\n');
callback(null, result);
});
} }
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) { supervisor.on('PROCESS_STATE_EXITED', function (headers, data) {
+37 -55
View File
@@ -105,8 +105,8 @@
} }
}, },
"cloudron-manifestformat": { "cloudron-manifestformat": {
"version": "1.4.0", "version": "1.6.0",
"from": "cloudron-manifestformat@1.4.0", "from": "cloudron-manifestformat@1.6.0",
"dependencies": { "dependencies": {
"java-packagename-regex": { "java-packagename-regex": {
"version": "1.0.0", "version": "1.0.0",
@@ -131,18 +131,17 @@
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz" "resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz"
}, },
"connect-lastmile": { "connect-lastmile": {
"version": "0.0.12", "version": "0.0.13",
"from": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz", "from": "connect-lastmile@0.0.13",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "2.1.3", "version": "2.1.3",
"from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "from": "debug@>=2.1.0 <2.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"dependencies": { "dependencies": {
"ms": { "ms": {
"version": "0.7.0", "version": "0.7.0",
"from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz", "from": "ms@0.7.0",
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz" "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
} }
} }
@@ -1288,69 +1287,69 @@
}, },
"dockerode": { "dockerode": {
"version": "2.2.2", "version": "2.2.2",
"from": "dockerode@2.2.2", "from": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz", "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz",
"dependencies": { "dependencies": {
"docker-modem": { "docker-modem": {
"version": "0.2.6", "version": "0.2.6",
"from": "docker-modem@>=0.2.0 <0.3.0", "from": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "0.7.4", "version": "0.7.4",
"from": "debug@>=0.7.4 <0.8.0", "from": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz"
}, },
"follow-redirects": { "follow-redirects": {
"version": "0.0.3", "version": "0.0.3",
"from": "follow-redirects@0.0.3", "from": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz" "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz"
}, },
"JSONStream": { "JSONStream": {
"version": "0.10.0", "version": "0.10.0",
"from": "JSONStream@0.10.0", "from": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"dependencies": { "dependencies": {
"jsonparse": { "jsonparse": {
"version": "0.0.5", "version": "0.0.5",
"from": "jsonparse@0.0.5", "from": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz" "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz"
}, },
"through": { "through": {
"version": "2.3.8", "version": "2.3.8",
"from": "through@>=2.2.7 <3.0.0", "from": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
} }
} }
}, },
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
"from": "querystring@0.2.0", "from": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz"
}, },
"readable-stream": { "readable-stream": {
"version": "1.0.33", "version": "1.0.33",
"from": "readable-stream@>=1.0.26-4 <1.1.0", "from": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"dependencies": { "dependencies": {
"core-util-is": { "core-util-is": {
"version": "1.0.1", "version": "1.0.1",
"from": "core-util-is@>=1.0.0 <1.1.0", "from": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz",
"resolved": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" "resolved": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
}, },
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"from": "isarray@0.0.1", "from": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"resolved": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" "resolved": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
}, },
"string_decoder": { "string_decoder": {
"version": "0.10.31", "version": "0.10.31",
"from": "string_decoder@>=0.10.0 <0.11.0", "from": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
}, },
"inherits": { "inherits": {
"version": "2.0.1", "version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0", "from": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" "resolved": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
} }
} }
@@ -1691,81 +1690,81 @@
}, },
"ldapjs": { "ldapjs": {
"version": "0.7.1", "version": "0.7.1",
"from": "ldapjs@*", "from": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz",
"dependencies": { "dependencies": {
"asn1": { "asn1": {
"version": "0.2.1", "version": "0.2.1",
"from": "asn1@0.2.1", "from": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz" "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz"
}, },
"assert-plus": { "assert-plus": {
"version": "0.1.5", "version": "0.1.5",
"from": "assert-plus@0.1.5", "from": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
}, },
"bunyan": { "bunyan": {
"version": "0.22.1", "version": "0.22.1",
"from": "bunyan@0.22.1", "from": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz" "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
}, },
"nopt": { "nopt": {
"version": "2.1.1", "version": "2.1.1",
"from": "nopt@2.1.1", "from": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz",
"dependencies": { "dependencies": {
"abbrev": { "abbrev": {
"version": "1.0.7", "version": "1.0.7",
"from": "abbrev@>=1.0.0 <2.0.0", "from": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
} }
} }
}, },
"pooling": { "pooling": {
"version": "0.4.6", "version": "0.4.6",
"from": "pooling@0.4.6", "from": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz",
"resolved": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz", "resolved": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz",
"dependencies": { "dependencies": {
"once": { "once": {
"version": "1.3.0", "version": "1.3.0",
"from": "once@1.3.0", "from": "https://registry.npmjs.org/once/-/once-1.3.0.tgz",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz" "resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz"
}, },
"vasync": { "vasync": {
"version": "1.4.0", "version": "1.4.0",
"from": "vasync@1.4.0", "from": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz", "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz",
"dependencies": { "dependencies": {
"jsprim": { "jsprim": {
"version": "0.3.0", "version": "0.3.0",
"from": "jsprim@0.3.0", "from": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
"dependencies": { "dependencies": {
"extsprintf": { "extsprintf": {
"version": "1.0.0", "version": "1.0.0",
"from": "extsprintf@1.0.0", "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz" "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz"
}, },
"json-schema": { "json-schema": {
"version": "0.2.2", "version": "0.2.2",
"from": "json-schema@0.2.2", "from": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
}, },
"verror": { "verror": {
"version": "1.3.3", "version": "1.3.3",
"from": "verror@1.3.3", "from": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz" "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz"
} }
} }
}, },
"verror": { "verror": {
"version": "1.1.0", "version": "1.1.0",
"from": "verror@1.1.0", "from": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
"dependencies": { "dependencies": {
"extsprintf": { "extsprintf": {
"version": "1.0.0", "version": "1.0.0",
"from": "extsprintf@1.0.0", "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz" "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz"
} }
} }
@@ -1938,23 +1937,6 @@
"from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz", "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz" "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz"
}, },
"nodejs-disks": {
"version": "0.2.1",
"from": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
"resolved": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
"dependencies": {
"async": {
"version": "0.2.10",
"from": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
},
"numeral": {
"version": "1.4.8",
"from": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz",
"resolved": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz"
}
}
},
"nodemailer": { "nodemailer": {
"version": "1.3.4", "version": "1.3.4",
"from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.3.4.tgz", "from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.3.4.tgz",
@@ -2261,9 +2243,9 @@
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.0.tgz" "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.0.tgz"
}, },
"safetydance": { "safetydance": {
"version": "0.0.16", "version": "0.0.19",
"from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz", "from": "safetydance@0.0.19",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz" "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
}, },
"semver": { "semver": {
"version": "4.3.6", "version": "4.3.6",
+3 -4
View File
@@ -18,9 +18,9 @@
"dependencies": { "dependencies": {
"async": "^1.2.1", "async": "^1.2.1",
"body-parser": "^1.13.1", "body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.4.0", "cloudron-manifestformat": "^1.6.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.12", "connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0", "connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5", "cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0", "cookie-session": "^1.1.0",
@@ -43,7 +43,6 @@
"mysql": "^2.7.0", "mysql": "^2.7.0",
"native-dns": "^0.7.0", "native-dns": "^0.7.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"nodejs-disks": "^0.2.1",
"nodemailer": "^1.3.0", "nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3", "nodemailer-smtp-transport": "^1.0.3",
"oauth2orize": "^1.0.1", "oauth2orize": "^1.0.1",
@@ -55,7 +54,7 @@
"passport-oauth2-client-password": "^0.1.2", "passport-oauth2-client-password": "^0.1.2",
"password-generator": "^1.0.0", "password-generator": "^1.0.0",
"proxy-middleware": "^0.13.0", "proxy-middleware": "^0.13.0",
"safetydance": "0.0.16", "safetydance": "0.0.19",
"semver": "^4.3.6", "semver": "^4.3.6",
"serve-favicon": "^2.2.0", "serve-favicon": "^2.2.0",
"split": "^1.0.0", "split": "^1.0.0",
+12 -1
View File
@@ -3,4 +3,15 @@
# If you change the infra version, be sure to put a warning # If you change the infra version, be sure to put a warning
# in the change log # in the change log
INFRA_VERSION=4 INFRA_VERSION=8
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.3.1
MYSQL_IMAGE=cloudron/mysql:0.3.2
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.1
MONGODB_IMAGE=cloudron/mongodb:0.3.1
REDIS_IMAGE=cloudron/redis:0.3.1 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.3.1
GRAPHITE_IMAGE=cloudron/graphite:0.3.3
+3
View File
@@ -34,6 +34,9 @@ ln -sfF "${DATA_DIR}/collectd" /etc/collectd
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
ln -s "${DATA_DIR}/nginx" /etc/nginx ln -s "${DATA_DIR}/nginx" /etc/nginx
########## mysql
cp "${container_files}/mysql.cnf" /etc/mysql/mysql.cnf
########## Enable services ########## Enable services
update-rc.d -f collectd defaults update-rc.d -f collectd defaults
+7
View File
@@ -0,0 +1,7 @@
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connection=50
+3
View File
@@ -24,3 +24,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV" Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
+10
View File
@@ -41,6 +41,9 @@ mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/mail" mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/graphite" mkdir -p "${DATA_DIR}/graphite"
mkdir -p "${DATA_DIR}/mysql"
mkdir -p "${DATA_DIR}/postgresql"
mkdir -p "${DATA_DIR}/mongodb"
mkdir -p "${DATA_DIR}/snapshots" mkdir -p "${DATA_DIR}/snapshots"
mkdir -p "${DATA_DIR}/addons" mkdir -p "${DATA_DIR}/addons"
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d" mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
@@ -53,6 +56,9 @@ echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_version
echo "Cleaning up snapshots" echo "Cleaning up snapshots"
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
# restart mysql to make sure it has latest config
service mysql restart
readonly mysql_root_password="password" readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box' mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
@@ -83,6 +89,10 @@ EOF
set_progress "28" "Setup collectd" set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf" cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
vda1_id=$(blkid -s UUID -o value /dev/vda1)
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
service collectd restart service collectd restart
set_progress "30" "Setup nginx" set_progress "30" "Setup nginx"
+3 -4
View File
@@ -193,12 +193,11 @@ LoadPlugin write_graphite
</Plugin> </Plugin>
<Plugin df> <Plugin df>
Device "/dev/vda1" FSType "tmpfs"
Device "/dev/loop0" MountPoint "/dev"
Device "/dev/loop1"
ReportByDevice true ReportByDevice true
IgnoreSelected false IgnoreSelected true
ValuesAbsolute true ValuesAbsolute true
ValuesPercentage true ValuesPercentage true
+11 -9
View File
@@ -27,11 +27,13 @@ if [[ -n "${existing_containers}" ]]; then
fi fi
# graphite # graphite
docker run --restart=always -d --name="graphite" \ graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-p 127.0.0.1:2003:2003 \ -p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \ -p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \ -p 127.0.0.1:8000:8000 \
-v "${DATA_DIR}/graphite:/app/data" cloudron/graphite:0.3.1 -v "${DATA_DIR}/graphite:/app/data" \
"${GRAPHITE_IMAGE}")
echo "Graphite container id: ${graphite_container_id}"
# mail # mail
mail_container_id=$(docker run --restart=always -d --name="mail" \ mail_container_id=$(docker run --restart=always -d --name="mail" \
@@ -39,7 +41,7 @@ mail_container_id=$(docker run --restart=always -d --name="mail" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \ -e "DOMAIN_NAME=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \ -v "${DATA_DIR}/box/mail:/app/data" \
cloudron/mail:0.3.0) "${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}" echo "Mail container id: ${mail_container_id}"
# mysql # mysql
@@ -52,8 +54,8 @@ EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \ mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \ -v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:r" \ -v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
cloudron/mysql:0.3.0) "${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}" echo "MySQL container id: ${mysql_container_id}"
# postgresql # postgresql
@@ -64,8 +66,8 @@ EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \ postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \ -v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:r" \ -v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
cloudron/postgresql:0.3.0) "${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}" echo "PostgreSQL container id: ${postgresql_container_id}"
# mongodb # mongodb
@@ -76,8 +78,8 @@ EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \ mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \ -v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:r" \ -v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
cloudron/mongodb:0.3.0) "${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}" echo "Mongodb container id: ${mongodb_container_id}"
if [[ "${infra_version}" == "none" ]]; then if [[ "${infra_version}" == "none" ]]; then
+2 -2
View File
@@ -665,7 +665,7 @@ function setupRedis(app, callback) {
name: 'redis-' + app.id, name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location), Hostname: config.appFqdn(app.location),
Tty: true, Tty: true,
Image: 'cloudron/redis:0.3.0', Image: 'cloudron/redis:0.3.1',
Cmd: null, Cmd: null,
Volumes: {}, Volumes: {},
VolumesFrom: [] VolumesFrom: []
@@ -675,7 +675,7 @@ function setupRedis(app, callback) {
var startOptions = { var startOptions = {
Binds: [ Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:r', redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw' redisDataDir + ':/var/lib/redis:rw'
], ],
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work // On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
+5 -5
View File
@@ -675,16 +675,16 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
function backupApp(app, addonsToBackup, callback) { function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToBackup, 'object'); assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
function canBackupApp(app) { function canBackupApp(app) {
// only backup apps that are installed or pending configure. Rest of them are in some // only backup apps that are installed or pending configure. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely) // state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|| app.installationState === appdb.ISTATE_PENDING_CONFIGURE app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|| app.installationState === appdb.ISTATE_PENDING_BACKUP app.installationState === appdb.ISTATE_PENDING_BACKUP ||
|| app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
} }
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy')); if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
+3 -5
View File
@@ -189,7 +189,6 @@ function createContainer(app, callback) {
} }
env.push('CLOUDRON=1'); env.push('CLOUDRON=1');
env.push('ADMIN_ORIGIN' + '=' + config.adminOrigin()); // ## remove
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin()); env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
env.push('API_ORIGIN' + '=' + config.adminOrigin()); env.push('API_ORIGIN' + '=' + config.adminOrigin());
@@ -202,8 +201,6 @@ function createContainer(app, callback) {
Tty: true, Tty: true,
Image: app.manifest.dockerImage, Image: app.manifest.dockerImage,
Cmd: null, Cmd: null,
Volumes: {},
VolumesFrom: [],
Env: env.concat(addonEnv), Env: env.concat(addonEnv),
ExposedPorts: exposedPorts ExposedPorts: exposedPorts
}; };
@@ -342,7 +339,8 @@ function startContainer(app, callback) {
RestartPolicy: { RestartPolicy: {
"Name": "always", "Name": "always",
"MaximumRetryCount": 0 "MaximumRetryCount": 0
} },
CpuShares: 512 // relative to 1024 for system processes
}; };
var container = docker.getContainer(app.containerId); var container = docker.getContainer(app.containerId);
@@ -673,7 +671,7 @@ function restore(app, callback) {
// note that configure is called after an infra update as well // note that configure is called after an infra update as well
function configure(app, callback) { function configure(app, callback) {
var locationChanged = app.oldConfig.location !== app.location; var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
async.series([ async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
+3 -3
View File
@@ -1,6 +1,6 @@
LoadPlugin "table" LoadPlugin "table"
<Plugin table> <Plugin table>
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat"> <Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
Instance "<%= appId %>-memory" Instance "<%= appId %>-memory"
Separator " \\n" Separator " \\n"
<Result> <Result>
@@ -10,7 +10,7 @@ LoadPlugin "table"
</Result> </Result>
</Table> </Table>
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes"> <Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
Instance "<%= appId %>-memory" Instance "<%= appId %>-memory"
Separator "\\n" Separator "\\n"
<Result> <Result>
@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result> </Result>
</Table> </Table>
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat"> <Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
Instance "<%= appId %>-cpu" Instance "<%= appId %>-cpu"
Separator " \\n" Separator " \\n"
<Result> <Result>
+32 -15
View File
@@ -24,6 +24,9 @@ var gLogger = {
fatal: console.error fatal: console.error
}; };
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admin,ou=groups,dc=cloudron';
function start(callback) { function start(callback) {
assert(typeof callback === 'function'); assert(typeof callback === 'function');
@@ -39,6 +42,9 @@ function start(callback) {
result.forEach(function (entry) { result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron'); var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var tmp = { var tmp = {
dn: dn.toString(), dn: dn.toString(),
attributes: { attributes: {
@@ -49,7 +55,8 @@ function start(callback) {
mail: entry.email, mail: entry.email,
displayname: entry.username, displayname: entry.username,
username: entry.username, username: entry.username,
samaccountname: entry.username // to support ActiveDirectory clients samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
} }
}; };
@@ -69,22 +76,32 @@ function start(callback) {
user.list(function (error, result){ user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString())); if (error) return next(new ldap.OperationsError(error.toString()));
// we only have an admin group var groups = [{
var dn = ldap.parseDN('cn=admin,ou=groups,dc=cloudron'); name: 'users',
admin: false
}, {
name: 'admins',
admin: true
}];
var tmp = { groups.forEach(function (group) {
dn: dn.toString(), var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
attributes: { var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
objectclass: ['group'],
cn: 'admin', var tmp = {
memberuid: result.filter(function (entry) { return entry.admin; }).map(function(entry) { return entry.id; }) dn: dn.toString(),
attributes: {
objectclass: ['group'],
cn: group.name,
memberuid: members.map(function(entry) { return entry.id; })
}
};
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
} }
}; });
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
}
res.end(); res.end();
}); });
+2 -1
View File
@@ -19,6 +19,7 @@ exports = module.exports = {
FEEDBACK_TYPE_FEEDBACK: 'feedback', FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket', FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
sendFeedback: sendFeedback sendFeedback: sendFeedback
}; };
@@ -288,7 +289,7 @@ function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof subject, 'string'); assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string'); assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK); assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
var mailOptions = { var mailOptions = {
from: config.get('adminEmail'), from: config.get('adminEmail'),
+1 -1
View File
@@ -25,4 +25,4 @@
</head> </head>
<body> <body class="oauth">
+34 -22
View File
@@ -1,32 +1,42 @@
<% include header %> <% include header %>
<center>
<h1>Login to <%= applicationName %></h1>
</center>
<% if (error) { %>
<center>
<br/><br/>
<h4 class="has-error"><%= error %></h4>
</center>
<% } %>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<form id="loginForm" action="" method="post"> <div class="card">
<input type="hidden" name="_csrf" value="<%= csrf %>"/> <div class="row">
<div class="form-group"> <div class="col-md-12" style="text-align: center;">
<label class="control-label" for="inputUsername">Username or Email</label> <img width="128" height="128" src="<%= applicationLogo %>"/>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required> <h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
<br/>
</div>
</div> </div>
<div class="form-group"> <br/>
<label class="control-label" for="inputPassword">Password</label> <% if (error) { %>
<input type="password" class="form-control" name="password" id="inputPassword" required> <div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div> </div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/> <% } %>
</form> <div class="row">
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a> <div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -34,6 +44,8 @@
<script> <script>
(function () { (function () {
'use strict';
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo; document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo;
+1 -1
View File
@@ -163,7 +163,7 @@ function setCertificate(req, res, next) {
function feedback(req, res, next) { function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET) return next(new HttpError(400, 'type must be either "ticket" or "feedback"')); if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string')); if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string')); if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
+31 -20
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
querystring = require('querystring'), querystring = require('querystring'),
util = require('util'), util = require('util'),
session = require('connect-ensure-login'), session = require('connect-ensure-login'),
settings = require('../settings.js'),
tokendb = require('../tokendb'), tokendb = require('../tokendb'),
appdb = require('../appdb'), appdb = require('../appdb'),
url = require('url'), url = require('url'),
@@ -188,37 +189,47 @@ function loginForm(req, res) {
var u = url.parse(req.session.returnTo, true); var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.'); if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
function render(applicationName) { var cloudronName = '';
function render(applicationName, applicationLogo) {
res.render('login', { res.render('login', {
adminOrigin: config.adminOrigin(), adminOrigin: config.adminOrigin(),
csrf: req.csrfToken(), csrf: req.csrfToken(),
cloudronName: cloudronName,
applicationName: applicationName, applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null error: req.query.error || null
}); });
} }
clientdb.get(u.query.client_id, function (error, result) { settings.getCloudronName(function (error, name) {
if (error) return sendError(req, res, 'Unknown OAuth client'); if (error) return sendError(req, res, 'Internal Error');
// Handle our different types of oauth clients cloudronName = name;
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME);
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME);
} else if (appId.indexOf('external-') === 0) {
return render('External Application');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) { clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials'); if (error) return sendError(req, res, 'Unknown OAuth client');
var applicationName = result.location || config.fqdn(); // Handle our different types of oauth clients
render(applicationName); var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/cloudron/avatar');
});
}); });
}); });
} }
+2 -1
View File
@@ -573,7 +573,8 @@ describe('App installation', function () {
docker.getContainer(appEntry.containerId).inspect(function (error, data) { docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok(); expect(error).to.not.be.ok();
expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ }); expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ });
expect(data.Config.Env).to.contain('ADMIN_ORIGIN=' + config.adminOrigin()); expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1'); expect(data.Config.Env).to.contain('CLOUDRON=1');
clientdb.getByAppId('addon-' + appResult.id, function (error, client) { clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
expect(error).to.not.be.ok(); expect(error).to.not.be.ok();
+11
View File
@@ -588,6 +588,17 @@ describe('Cloudron', function () {
}); });
}); });
it('succeeds with app type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without description', function (done) { it('fails without description', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback') request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject' }) .send({ type: 'ticket', subject: 'some subject' })
+7 -3
View File
@@ -2,6 +2,10 @@
set -eu -o pipefail set -eu -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
source ${SOURCE_DIR}/setup/INFRA_VERSION
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)" readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)" readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)" readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)"
@@ -20,7 +24,7 @@ start_postgresql() {
docker rm -f postgresql 2>/dev/null 1>&2 || true docker rm -f postgresql 2>/dev/null 1>&2 || true
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh cloudron/postgresql:0.3.0 >/dev/null docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
} }
start_mysql() { start_mysql() {
@@ -36,7 +40,7 @@ start_mysql() {
docker rm -f mysql 2>/dev/null 1>&2 || true docker rm -f mysql 2>/dev/null 1>&2 || true
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh cloudron/mysql:0.3.0 >/dev/null docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
} }
start_mongodb() { start_mongodb() {
@@ -52,7 +56,7 @@ start_mongodb() {
docker rm -f mongodb 2>/dev/null 1>&2 || true docker rm -f mongodb 2>/dev/null 1>&2 || true
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh cloudron/mongodb:0.3.0 >/dev/null docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
} }
start_mysql start_mysql
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -eu -o pipefail
if [[ $EUID -ne 0 ]]; then
echo "This script should be run as root." >&2
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [ $# -lt 1 ]; then
echo "Usage: collectlogs.sh <program>"
exit 1
fi
readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
tail --lines=100 /var/log/supervisor/${program_name}.log
echo
echo
echo "dmesg"
echo "-----"
dmesg | tail --lines=100
echo
echo
echo "docker"
echo "------"
tail --lines=100 /var/log/upstart/docker.log
echo
echo
+1 -1
View File
@@ -54,7 +54,7 @@ function uninitialize(callback) {
function startNextTask() { function startNextTask() {
if (gPendingTasks.length === 0) return; if (gPendingTasks.length === 0) return;
assert(Object.keys(gActiveTasks).length === 0); // since we allow only one task at a time assert.strictEqual(Object.keys(gActiveTasks).length, 0); // since we allow only one task at a time
startAppTask(gPendingTasks.shift()); startAppTask(gPendingTasks.shift());
} }
+21 -18
View File
@@ -2,21 +2,24 @@
set -eu set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source ${SOURCE_DIR}/setup/INFRA_VERSION
# reset sudo timestamp to avoid wrong success # reset sudo timestamp to avoid wrong success
sudo -k || sudo --reset-timestamp sudo -k || sudo --reset-timestamp
# checks if all scripts are sudo access # checks if all scripts are sudo access
scripts=("${SOURCE_DIR}/scripts/rmappdir.sh" \ scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
"${SOURCE_DIR}/scripts/createappdir.sh" \ "${SOURCE_DIR}/src/scripts/createappdir.sh" \
"${SOURCE_DIR}/scripts/reloadnginx.sh" \ "${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
"${SOURCE_DIR}/scripts/backupbox.sh" \ "${SOURCE_DIR}/src/scripts/backupbox.sh" \
"${SOURCE_DIR}/scripts/backupapp.sh" \ "${SOURCE_DIR}/src/scripts/backupapp.sh" \
"${SOURCE_DIR}/scripts/restoreapp.sh" \ "${SOURCE_DIR}/src/scripts/restoreapp.sh" \
"${SOURCE_DIR}/scripts/reboot.sh" \ "${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/scripts/backupswap.sh" \ "${SOURCE_DIR}/src/scripts/backupswap.sh" \
"${SOURCE_DIR}/scripts/reloadcollectd.sh") "${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh")
for script in "${scripts[@]}"; do for script in "${scripts[@]}"; do
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
@@ -36,23 +39,23 @@ if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
exit 1 exit 1
fi fi
if ! docker inspect cloudron/redis:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/redis:0.3.0 for tests to run" echo "docker pull ${REDIS_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/mysql:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/mysql:0.3.0 for tests to run" echo "docker pull ${MYSQL_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/postgresql:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/postgresql:0.3.0 for tests to run" echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/mongodb:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/mongodb:0.3.0 for tests to run" echo "docker pull ${MONGODB_IMAGE} for tests to run"
exit 1 exit 1
fi fi
+263
View File
@@ -0,0 +1,263 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
require('supererror', { splatchError: true});
var database = require('../database.js'),
expect = require('expect.js'),
EventEmitter = require('events').EventEmitter,
async = require('async'),
user = require('../user.js'),
config = require('../config.js'),
ldapServer = require('../ldap.js'),
ldap = require('ldapjs');
var USER_0 = {
username: 'foobar0',
password: 'password0',
email: 'foo0@bar.com'
};
var USER_1 = {
username: 'foobar1',
password: 'password1',
email: 'foo1@bar.com'
};
function setup(done) {
async.series([
database.initialize.bind(null),
database._clear.bind(null),
ldapServer.start.bind(null),
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, true, null),
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0)
], done);
}
function cleanup(done) {
database._clear(done);
}
describe('Ldap', function () {
before(setup);
after(cleanup);
describe('bind', function () {
it('fails for nonexisting user', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=doesnotexist,ou=users,dc=cloudron', 'password', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
done();
});
});
it('fails with wrong password', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', 'wrongpassword', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
done();
});
});
it('succeeds', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
expect(error).to.be(null);
done();
});
});
});
describe('search users', function () {
it ('fails for non existing tree', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '(&(l=Seattle)(email=*@foo.com))'
};
client.search('o=example', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
result.on('error', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
done();
});
result.on('end', function (result) {
done(new Error('Should not succeed. Status ' + result.status));
});
});
});
it ('succeeds with basic filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectcategory=person'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[1].username).to.equal(USER_1.username);
done();
});
});
});
it ('succeeds with username wildcard filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectcategory=person)(username=foobar*)'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[1].username).to.equal(USER_1.username);
done();
});
});
});
it ('succeeds with username filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectcategory=person)(username=' + USER_0.username + ')'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[0].memberof.length).to.equal(2);
done();
});
});
});
});
describe('search groups', function () {
it ('succeeds with basic filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectclass=group'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.username);
done();
});
});
});
it ('succeeds with cn wildcard filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(cn=*)'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.username);
done();
});
});
});
it('succeeds with memberuid filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(memberuid=' + USER_1.username + ')'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
done();
});
});
});
});
});
+11
View File
@@ -63,6 +63,17 @@ angular.module('Application').service('AppStore', ['$http', 'Client', function (
}); });
}; };
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) { AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm')); if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
+111 -18
View File
@@ -120,6 +120,14 @@ html {
.grid-item { .grid-item {
padding: 10px; padding: 10px;
min-width: 200px; min-width: 200px;
overflow: hidden;
}
.grid-item:hover .grid-item-bottom {
@media(min-width:768px) {
opacity: 1;
right: 10px;
}
} }
.grid-item-content { .grid-item-content {
@@ -132,10 +140,39 @@ html {
padding: 10px 15px; padding: 10px 15px;
} }
.grid-item-bottom { .grid-item-top-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 24px;
}
.grid-item-bottom-mobile {
padding: 10px 15px; padding: 10px 15px;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
background-color: white background-color: white;
@media(min-width:768px) {
display: none;
}
}
.grid-item-bottom {
display: none;
padding: 10px 15px;
border-top: 1px solid #ddd;
background-color: white;
@media(min-width:768px) {
display: block;
position: absolute;
top: 0;
right: -10px;
opacity: 0;
background-color: transparent;
transition: all 250ms;
}
} }
// ---------------------------- // ----------------------------
@@ -195,15 +232,53 @@ html {
} }
.appstore-category-link { .appstore-category-link {
display: block;
padding: 10px; padding: 10px;
margin: 0; margin: 0;
overflow: hidden;
color: black; color: black;
color: inherit;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
display: block;
font-size: 18px; font-size: 18px;
&:hover,
&:focus,
&.category-active {
text-decoration: none;
background-color: white;
color: black;
}
&.category-active {
background-color: $navbar-default-link-hover-color;
color: white;
}
}
.appstore-category-missing {
padding: 10px;
background-color: white;
p {
font-weight: bold;
}
textarea {
display: block;
width: 100%;
height: 50px;
margin-bottom: 10px;
transition: all 250ms ease-out;
&:focus {
height: 200px;
}
}
button {
width: 100%;
}
} }
.appstore-install-description { .appstore-install-description {
@@ -226,14 +301,6 @@ html {
color: $text-muted; color: $text-muted;
} }
.appstore-category-link:hover,
.appstore-category-link:focus,
.appstore-category-link.category-active {
text-decoration: none;
background-color: $navbar-default-link-hover-color;
color: white;
}
.appstore-item-rating { .appstore-item-rating {
color: $navbar-default-link-hover-color; color: $navbar-default-link-hover-color;
} }
@@ -333,12 +400,9 @@ html {
.grid-item-top .progress { .grid-item-top .progress {
border-radius: 0; border-radius: 0;
box-shadown: none; box-shadown: none;
margin-left: -15px; margin-top: 10px;
margin-right: -15px;
margin-bottom: -11px;
margin-top: 9px;
width: inherit; width: inherit;
height: 2px; height: 10px;
} }
.grid-item-top .progress-bar { .grid-item-top .progress-bar {
@@ -640,6 +704,33 @@ footer a {
} }
// ----------------------------
// Oauth classes
// ----------------------------
.oauth {
height: 100%;
width: 100%;
padding: 0;
background: #F7F7F7;
h1 {
font-size: 33px;
}
.card {
max-width: none;
padding: 20px;
text-align: left;
margin-top: 15px;
@media(min-width:768px) {
margin-top: 20%;
}
}
}
// ---------------------------- // ----------------------------
// Graphs classes // Graphs classes
// ---------------------------- // ----------------------------
@@ -790,6 +881,8 @@ $graphs-success-alt: lighten(#27CE65, 20%);
.support { .support {
max-width: 600px;
h3 { h3 {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
+2
View File
@@ -89,6 +89,8 @@
</div> </div>
</div> </div>
<br/>
<div style="max-width: 600px; margin: 0 auto;"> <div style="max-width: 600px; margin: 0 auto;">
<div class="text-left"> <div class="text-left">
<h1>Account</h1> <h1>Account</h1>
+52 -25
View File
@@ -43,6 +43,7 @@
<option value="roleUser">Visible only to Cloudron users</option> <option value="roleUser">Visible only to Cloudron users</option>
</select> </select>
</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>
<br/> <br/>
<br/> <br/>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }"> <div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
@@ -53,7 +54,7 @@
</form> </form>
</fieldset> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer ">
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button> <button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button> <button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button> <button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
@@ -182,6 +183,9 @@
</script> </script>
<div class="content"> <div class="content">
<br/>
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0"> <div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<div class="col-lg-12"> <div class="col-lg-12">
<h1>Installed Applications</h1> <h1>Installed Applications</h1>
@@ -200,11 +204,12 @@
</div> </div>
<br/> <br/>
<div class="row"> <div class="row">
<div class="col-xs-12 text-left"> <div class="col-xs-12 text-center">
<div style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">{{ app.location || app.fqdn }}</div> <div class="grid-item-top-title">{{ app.location || app.fqdn }}</div>
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden"> <div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ app | installationStateLabel }} {{ app | installationStateLabel }}
</div> </div>
<br ng-hide="app | installationActive"/>
<div ng-show="app | installationActive"> <div ng-show="app | installationActive">
<div class="progress progress-striped active"> <div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div> <div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
@@ -213,31 +218,53 @@
</div> </div>
</div> </div>
</div> </div>
</a> <div class="grid-item-bottom-mobile" ng-show="user.admin">
<div class="grid-item-bottom" ng-show="user.admin"> <div class="row">
<div class="row"> <div class="col-xs-4 text-left">
<div class="col-xs-4 text-left"> <a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true">
<a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true"> <i class="fa fa-undo scale"></i>
<i class="fa fa-undo scale"></i> </a>
</a>
<a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true"> <a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true">
<i class="fa fa-wrench scale"></i> <i class="fa fa-wrench scale"></i>
</a> </a>
</div> </div>
<div class="col-xs-4 text-center"> <div class="col-xs-4 text-center">
<!-- we check the version here because the box updater does not know when an app gets updated --> <!-- we check the version here because the box updater does not know when an app gets updated -->
<a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)"> <a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<i class="fa fa-arrow-up text-success scale"></i> <i class="fa fa-arrow-up text-success scale"></i>
</a> </a>
</div> </div>
<div class="col-xs-4 text-right"> <div class="col-xs-4 text-right">
<a href="" ng-click="showUninstall(app)"> <a href="" ng-click="showUninstall(app)">
<i class="fa fa-remove scale"></i> <i class="fa fa-remove scale"></i>
</a> </a>
</div>
</div> </div>
</div> </div>
</div> <div class="grid-item-bottom" ng-show="user.admin">
<br/>
<div>
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
</div>
<div ng-show="(app | installError) === true">
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
</div>
<div ng-show="(app | installSuccess) == true">
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
</div>
<br/>
</div>
</a>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -334,7 +334,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}; };
// setup all the dialog focus handling // setup all the dialog focus handling
['appConfigureModal', 'appUninstallModal', 'appUpdateModal'].forEach(function (id) { ['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () { $('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus(); $(this).find("[autofocus]:first").focus();
}); });
+47 -1
View File
@@ -64,6 +64,47 @@
</div> </div>
</div> </div>
<!-- Modal feedback -->
<div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">App Feedback</h4>
</div>
<div class="modal-body">
<fieldset>
<form name="feedbackForm" ng-submit="submitFeedback()">
<div ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</div>
<textarea class="form-control" id="feedbackDescriptionTextarea" cols="3" ng-model="feedback.description" ng-minlength="1" required placeholder="Name, Category, Links ..." autofocus></textarea>
<input class="ng-hide" type="submit" ng-disabled="feedbackForm.$invalid || feedback.busy"/>
</form>
</fieldset>
</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="submitFeedback()" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-fw fa-paper-plane"></i> Submit</button>
</div>
</div>
</div>
</div>
<!-- Modal app not found -->
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">App not found</h4>
</div>
<div class="modal-body">
There is no such app <b>{{ appNotFound.appId }}</b><span ng-show="appNotFound.version"> with version <b>{{ appNotFound.version }}</b></span>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
<div> <div>
<div class="row-no-margin"> <div class="row-no-margin">
<div class="col-md-2"> <div class="col-md-2">
@@ -98,6 +139,10 @@
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki">Wiki</a> <a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki">Wiki</a>
<br/> <br/>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'testing' }" category="testing" ng-show="config.developerMode">Testing</a> <a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'testing' }" category="testing" ng-show="config.developerMode">Testing</a>
<br/>
<br/>
<br/>
<a href="" ng-click="showFeedbackModal()">Missing an app? Let us know.</a>
</div> </div>
<div class="col-md-10" ng-show="ready && apps.length"> <div class="col-md-10" ng-show="ready && apps.length">
<div class="row-no-margin"> <div class="row-no-margin">
@@ -117,7 +162,8 @@
</div> </div>
</div> </div>
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="ready && !apps.length"> <div class="col-md-10 animateMeOpacity loading-banner" ng-show="ready && !apps.length">
<h3 class="text-muted">No applications in this category</h3> <h3 class="text-muted">No applications in this category.</h3>
<a href="" ng-click="showFeedbackModal()"><h3>Let us know if you miss something.</h3></a>
</div> </div>
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="!ready"> <div class="col-md-10 animateMeOpacity loading-banner" ng-show="!ready">
<h2><i class="fa fa-spinner fa-pulse"></i> Loading</h2> <h2><i class="fa fa-spinner fa-pulse"></i> Loading</h2>
+69 -6
View File
@@ -20,6 +20,47 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
mediaLinks: [] mediaLinks: []
}; };
$scope.appNotFound = {
appId: '',
version: ''
};
$scope.feedback = {
error: null,
success: false,
subject: 'App feedback',
description: '',
type: 'app'
};
function resetFeedback() {
$scope.feedback.description = '';
$scope.feedbackForm.$setUntouched();
$scope.feedbackForm.$setPristine();
}
$scope.submitFeedback = function () {
$scope.feedback.busy = true;
$scope.feedback.success = false;
$scope.feedback.error = null;
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
if (error) {
$scope.feedback.error = error;
} else {
$scope.feedback.success = true;
$('#feedbackModal').modal('hide');
resetFeedback();
}
$scope.feedback.busy = false;
});
};
$scope.showFeedbackModal = function () {
$('#feedbackModal').modal('show');
};
function getAppList(callback) { function getAppList(callback) {
AppStore.getApps(function (error, apps) { AppStore.getApps(function (error, apps) {
@@ -132,6 +173,13 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
} }
}; };
$scope.showAppNotFound = function (appId, version) {
$scope.appNotFound.appId = appId;
$scope.appNotFound.version = version;
$('#appNotFoundModal').modal('show');
};
$scope.doInstall = function () { $scope.doInstall = function () {
$scope.appInstall.busy = true; $scope.appInstall.busy = true;
$scope.appInstall.error.other = null; $scope.appInstall.error.other = null;
@@ -189,11 +237,26 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// show install app dialog immediately if an app id was passed in the query // show install app dialog immediately if an app id was passed in the query
if ($routeParams.appId) { if ($routeParams.appId) {
var found = apps.filter(function (app) { if ($routeParams.version) {
return (app.id === $routeParams.appId) && ($routeParams.version ? $routeParams.version === app.manifest.version : true); AppStore.getAppByIdAndVersion($routeParams.appId, $routeParams.version, function (error, result) {
}); if (error) {
if (found.length) { $scope.showAppNotFound($routeParams.appId, $routeParams.version);
$scope.showInstall(found[0]); console.error(error);
return;
}
$scope.showInstall(result);
});
} else {
var found = apps.filter(function (app) {
return (app.id === $routeParams.appId) && ($routeParams.version ? $routeParams.version === app.manifest.version : true);
});
if (found.length) {
$scope.showInstall(found[0]);
} else {
$scope.showAppNotFound($routeParams.appId, null);
}
} }
} }
@@ -204,7 +267,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
refresh(); refresh();
// setup all the dialog focus handling // setup all the dialog focus handling
['appInstallModal'].forEach(function (id) { ['appInstallModal', 'feedbackModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () { $('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus(); $(this).find("[autofocus]:first").focus();
}); });
+2
View File
@@ -1,4 +1,6 @@
<br/>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1>Graphs</h1> <h1>Graphs</h1>
+8
View File
@@ -107,6 +107,14 @@
</div> </div>
</div> </div>
<br/>
<div style="max-width: 600px; margin: 0 auto;">
<div class="text-left">
<h1>Settings</h1>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin"> <div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left"> <div class="text-left">
<h3>Backups</h3> <h3>Backups</h3>
+5 -3
View File
@@ -1,4 +1,6 @@
<div class="content support"> <div class="content support">
<br/>
<div> <div>
<div class="text-left"> <div class="text-left">
<h1>Support</h1> <h1>Support</h1>
@@ -33,6 +35,7 @@
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required> <select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required>
<option value="feedback">Enhancement / Idea</option> <option value="feedback">Enhancement / Idea</option>
<option value="ticket">Bug Report</option> <option value="ticket">Bug Report</option>
<option value="app">Missing App</option>
</select> </select>
</div> </div>
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }"> <div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
@@ -51,6 +54,5 @@
</div> </div>
</div> </div>
<br/> <!-- Offset the footer -->
<br/> <br/><br/>
<br/>
+1 -1
View File
@@ -77,7 +77,7 @@
<div class="cloudron-model-item-content shadow" ng-class="{ 'selected': size.slug === currentSize.slug }" style="height: {{ 120 + $index * 30 }}px"> <div class="cloudron-model-item-content shadow" ng-class="{ 'selected': size.slug === currentSize.slug }" style="height: {{ 120 + $index * 30 }}px">
<!-- <img src="img/box.png" style="transform: scale({{ size.price/50.0 }});"/><br/> --> <!-- <img src="img/box.png" style="transform: scale({{ size.price/50.0 }});"/><br/> -->
<h3>{{ size.name }}</h3> <h3>{{ size.name }}</h3>
<h5>${{ size.price }}/mo</h5> <h5>${{ (size.price/100).toFixed() }}/mo</h5>
<button class="btn btn-success" ng-disabled="busy" ng-hide="size.slug === currentSize.slug" ng-click="showUpgradeConfirm(size)">Upgrade</button> <button class="btn btn-success" ng-disabled="busy" ng-hide="size.slug === currentSize.slug" ng-click="showUpgradeConfirm(size)">Upgrade</button>
<button class="btn btn-success" ng-show="size.slug === currentSize.slug" data-toggle="tooltip" data-placement="top" title="Your Current Model"><i class="fa fa-check"></i></button> <button class="btn btn-success" ng-show="size.slug === currentSize.slug" data-toggle="tooltip" data-placement="top" title="Your Current Model"><i class="fa fa-check"></i></button>
</div> </div>
+3
View File
@@ -80,6 +80,9 @@
</div> </div>
<div class="content"> <div class="content">
<br/>
<div> <div>
<div class="text-left"> <div class="text-left">
<h1>Users <button class="btn btn-primary btn-outline pull-right" data-toggle="modal" data-target="#userAddModal"><i class="fa fa-user-plus"></i> New User</button></h1> <h1>Users <button class="btn btn-primary btn-outline pull-right" data-toggle="modal" data-target="#userAddModal"><i class="fa fa-user-plus"></i> New User</button></h1>