Compare commits

..

103 Commits

Author SHA1 Message Date
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
Johannes Zellner 0b4256a992 Unify feedback and ticket forms 2015-08-05 14:27:04 +02:00
Johannes Zellner 7d58d69389 Fix setup step on ng-enter 2015-08-04 22:17:58 +02:00
Johannes Zellner 864dd5bf26 New shrinkwrap for ldapjs without dtrace-provider
We have to install ldapjs with --no-optional

Fixes #460
2015-08-04 20:43:36 +02:00
Johannes Zellner abdde7a950 Put the correct faq and docs links 2015-08-04 19:36:05 +02:00
Johannes Zellner 8bcbd860be Add unit tests for feedback route and fix the route 2015-08-04 16:59:35 +02:00
Johannes Zellner be61c42fe8 Send feedback and tickets to support@cloudron.io 2015-08-04 16:05:20 +02:00
Johannes Zellner 6d5afc2d75 Give support form headers more space 2015-08-04 16:04:44 +02:00
Johannes Zellner 88d905e8cc Add support form feedback 2015-08-04 16:01:50 +02:00
Johannes Zellner d8ccc766b9 Add text-bold class 2015-08-04 16:01:33 +02:00
Johannes Zellner d22e0f0483 mailer functions only enqueue, respond immediately 2015-08-04 15:39:14 +02:00
Johannes Zellner c8f6973312 Do not send adminEmail for feedback mails 2015-08-04 14:56:43 +02:00
Johannes Zellner 3f0f0048bc add missing email format 2015-08-04 14:52:40 +02:00
Johannes Zellner 88643f0875 Add missing %> 2015-08-04 14:49:43 +02:00
Johannes Zellner e11bb10bb8 The requested function is in mailer 2015-08-04 14:45:42 +02:00
Johannes Zellner 7b9930c7f0 Do the feedback and ticket form plumbing 2015-08-04 14:44:39 +02:00
Johannes Zellner da48e32bcc Add feedback route 2015-08-04 14:31:40 +02:00
Johannes Zellner 57e2803bd2 Add feedback email template 2015-08-04 14:31:33 +02:00
Johannes Zellner 0d1ba01d65 Add initial support view 2015-08-04 11:33:36 +02:00
Girish Ramakrishnan 95cbec19af Copy the manifest because changes are made to it
Because of that, manifest verification fails (isNew property appears in manfiest)
2015-08-03 21:31:15 -07:00
Girish Ramakrishnan cc97654b23 Fix text 2015-08-02 19:02:45 -07:00
Girish Ramakrishnan 5bb983f175 Send docker log in crash email 2015-08-01 21:42:34 -07:00
Johannes Zellner 7cb6434de1 Move avatar name below the selected avatar preview 2015-07-30 16:38:10 +02:00
Johannes Zellner cb1b495da2 Revert "Actually remove dtrace dep"
This reverts commit 2b9bf6d019.
2015-07-30 14:53:53 +02:00
Girish Ramakrishnan e134136d59 previewAvatar seems to be defined in step1 and step2 2015-07-29 18:10:25 -07:00
Girish Ramakrishnan 85a681e330 There is no step4 2015-07-29 17:09:05 -07:00
Girish Ramakrishnan dc5c0fd830 setPreviewAvatar only in avatar selection step 2015-07-29 16:30:32 -07:00
Girish Ramakrishnan e7bf8452ab randomize default avatar 2015-07-29 16:11:37 -07:00
Girish Ramakrishnan 157f972b20 Decrease size of image preview 2015-07-29 16:11:20 -07:00
Girish Ramakrishnan b36028dc11 Pick -> Choose 2015-07-29 15:55:41 -07:00
Girish Ramakrishnan 70092ec559 Ensure image got loaded before setting the preview 2015-07-29 15:53:58 -07:00
Girish Ramakrishnan 56d740d597 Merge welcome step and step2 2015-07-29 15:11:34 -07:00
Girish Ramakrishnan ed55e52363 Actually remove dtrace dep
Use --no-optional when installing dtrace
2015-07-29 10:15:25 -07:00
Johannes Zellner 89c36ae6a9 Do not show the update page if update failed 2015-07-29 14:19:15 +02:00
Johannes Zellner 3027c119fe Use angular in update dialog and show errors 2015-07-29 14:02:31 +02:00
Johannes Zellner 4f129102a8 Use -1 for progress to indicate an error 2015-07-29 13:53:36 +02:00
Johannes Zellner 2dd6bb0c67 Rename upgradeError to updateError in update 2015-07-29 13:52:59 +02:00
Johannes Zellner b928b08a4c Reset update progress on update failure 2015-07-29 12:41:19 +02:00
Johannes Zellner 9dcc6e68a4 Use new avatar set
Fixes #456
2015-07-29 11:13:59 +02:00
Girish Ramakrishnan 452e67be54 This is probably obvious 2015-07-28 23:12:53 -07:00
Girish Ramakrishnan 9e0611f6d8 Improve wording of wizard 2015-07-28 23:09:06 -07:00
Girish Ramakrishnan ad3392ef2e model is queried from appstore 2015-07-28 17:08:32 -07:00
Girish Ramakrishnan 71e8abf081 define adminOrigin in splashpage.sh 2015-07-28 16:52:27 -07:00
Girish Ramakrishnan 46172e76c6 Keep updater arguments sorted for readability 2015-07-28 16:03:32 -07:00
Girish Ramakrishnan 7e639bd0e2 Release update/upgrade lock only on error 2015-07-28 15:28:10 -07:00
Girish Ramakrishnan 7a9af5373b Check percent value before redirecting to update.html 2015-07-28 14:43:49 -07:00
Girish Ramakrishnan 3ea7a11d97 Set progress completion error messages 2015-07-28 14:40:22 -07:00
Girish Ramakrishnan f582ba1ba7 console.error any backup error message for now 2015-07-28 14:30:40 -07:00
Girish Ramakrishnan b96fc2bc56 initialize percent 2015-07-28 14:28:53 -07:00
Girish Ramakrishnan 48c16277f0 Create error object properly 2015-07-28 14:22:34 -07:00
Girish Ramakrishnan 4ad4ff0b10 Use progress.set in upgrade/update code paths 2015-07-28 14:22:08 -07:00
Girish Ramakrishnan 25f05e5abd Add missing ; 2015-07-28 13:09:24 -07:00
Girish Ramakrishnan 7c214a9181 log update and upgrade errors 2015-07-28 10:03:52 -07:00
Johannes Zellner d66b1eef59 Better support for active directory clients 2015-07-28 18:39:16 +02:00
Girish Ramakrishnan 58f52b90f8 better debug on what is being autoupdated 2015-07-28 09:37:46 -07:00
Girish Ramakrishnan edb67db4ea Remove unnecessary debug making logs very verbose 2015-07-28 09:32:19 -07:00
Johannes Zellner 733014d8d9 No need to guess the apiOrigin anymore, we redirect now
Fixes #436
2015-07-28 14:03:48 +02:00
Johannes Zellner 4980f79688 Show link to referrer in appstatus 2015-07-28 14:01:51 +02:00
Johannes Zellner 3d8b90f5c8 Redirect on app error to webadmin appstatus page
Part of #436
2015-07-28 13:46:58 +02:00
Johannes Zellner eea547411b Show testing badges in appstore view 2015-07-28 13:21:23 +02:00
Johannes Zellner af682e5bb1 Fix the app icons in the install app grid 2015-07-28 13:06:55 +02:00
Johannes Zellner 739dcfde8b Show version and author in install dialog 2015-07-28 12:53:33 +02:00
Johannes Zellner 1db58dd78d Support ?version in direct appstore URLs
Fixes #454
2015-07-28 11:49:04 +02:00
Johannes Zellner 947137b3f9 Ensure we have a fallback avatar 2015-07-28 11:28:06 +02:00
Johannes Zellner 509e2caa83 Also show avatar in nakeddomain error page 2015-07-28 11:19:13 +02:00
Johannes Zellner a0e67daa52 Use avatar in error page 2015-07-28 11:18:55 +02:00
Johannes Zellner 32584f3a90 Fix long lasting navbar padding issue 2015-07-28 10:57:48 +02:00
Johannes Zellner 3513f321fb Reload webadmin in case the avatar changes
Fixes #452
2015-07-28 10:50:33 +02:00
Johannes Zellner 8aaccbba55 Show avatar in navbar 2015-07-28 10:49:56 +02:00
Johannes Zellner 31ab86a97f Show avatar as favicon 2015-07-28 10:40:10 +02:00
Girish Ramakrishnan 2c0786eb37 Use ldapjs from github directly
The 0.7.x ldapjs is over a year old and uses dtrace as a dep which
causes issues when rebuilding.
2015-07-27 13:06:30 -07:00
Johannes Zellner 3db8ebf97f Ensure the appstore ui can operate always on manifest.tags 2015-07-27 19:29:25 +02:00
Johannes Zellner 804105ce2b Add testing section in appstore and mark testing apps
This is not some final design to indicate which app is in testing
but the logistics are there, mainly css from now

Fixes #451
2015-07-27 17:09:59 +02:00
Johannes Zellner c4bb56dc95 Show non published apps in webadmin 2015-07-27 16:34:37 +02:00
Johannes Zellner 87c76a3eb3 Read apps from actual response body 2015-07-27 16:27:50 +02:00
Johannes Zellner 6bceff14ec Add proxy api to get non approved app listings 2015-07-27 14:00:44 +02:00
Girish Ramakrishnan 6b62561706 Add mandatory addons object 2015-07-24 06:59:34 -07:00
Girish Ramakrishnan d558c06803 Add missing semicolon 2015-07-24 06:53:07 -07:00
Girish Ramakrishnan ef9508ccc5 Use BOX_ENV instead of NODE_ENV
Let NODE_ENV be used by node modules and always be set to production

Fixes #453
2015-07-24 01:42:28 -07:00
Girish Ramakrishnan ec8342c2ce Better progress messages 2015-07-23 22:50:58 -07:00
Girish Ramakrishnan 6839f47f99 Fix typo 2015-07-23 14:30:15 -07:00
Girish Ramakrishnan d32990d0e5 Set server_names_hash_bucket_size
e2e tests fail like so when the hostnames are long

Thu, 23 Jul 2015 20:40:23 GMT box:apptask test8629 writing config to /home/yellowtent/data/nginx/applications/a3822f18-2f95-4b73-b8e9-2983dfcaae31.conf
Thu, 23 Jul 2015 20:40:23 GMT box:shell.js reloadNginx execFile: /usr/bin/sudo -S /home/yellowtent/box/src/scripts/reloadnginx.sh
Thu, 23 Jul 2015 20:40:24 GMT box:shell.js reloadNginx (stderr): nginx: [emerg] could not build the server_names_hash, you should increase server_names_hash_bucket_size: 64

Thu, 23 Jul 2015 20:40:24 GMT box:shell.js reloadNginx code: 1, signal: null
Thu, 23 Jul 2015 20:40:24 GMT box:apptask test8629 error installing app: Error: Exited with error 1 signal null
Thu, 23 Jul 2015 20:40:24 GMT box:apptask test8629 installationState: pending_install progress: 15, Configure nginx
^[[1m^[[31mERROR^[[39m^[[22m Exited with error 1 signal null ^[[1m[ /home/yellowtent/box/src/apptask.js:909:32 ]^[[22m
^[[32mstack: ^[[39m
  """
    Error: Exited with error 1 signal null
        at ChildProcess.<anonymous> (/home/yellowtent/box/src/shell.js:38:53)
        at ChildProcess.emit (events.js:110:17)
        at Process.ChildProcess._handle.onexit (child_process.js:1074:12)
  """
^[[32mmessage: ^[[39mExited with error 1 signal null
2015-07-23 13:55:46 -07:00
Johannes Zellner 71dbe21fc3 Set no-cache for the avatar 2015-07-23 16:34:44 +02:00
Johannes Zellner f36616abbb Remove developerMode from update provisioning data
Finally fixes #442
2015-07-23 13:31:39 +02:00
Johannes Zellner db6d6d565f Remove developerMode from config.js 2015-07-23 13:26:30 +02:00
Johannes Zellner 5f3fc68b5e Fixup developers test with new developer mode setting 2015-07-23 13:19:51 +02:00
Johannes Zellner bdca5e343b Fixup clients test with new developer mode setting 2015-07-23 13:16:36 +02:00
Johannes Zellner 58cf712e71 Fix apps-test to use settings developerMode 2015-07-23 12:59:47 +02:00
Johannes Zellner ca7e67ea4f Use developerMode from settings instead of config 2015-07-23 12:52:04 +02:00
Johannes Zellner b202043019 Add developerMode to settings
Part of #442
2015-07-23 12:42:56 +02:00
Johannes Zellner 19fef4c337 Add missing appId key to access app updateInfo 2015-07-23 07:21:05 +02:00
Johannes Zellner 7b864fed04 Only log error if NOOP_CALLBACK got an error 2015-07-23 07:17:30 +02:00
87 changed files with 1404 additions and 738 deletions
+9 -34
View File
@@ -7,48 +7,23 @@
// !! No console.log() allowed
// !! Do not set DEBUG
var supervisor = require('supervisord-eventlistener'),
assert = require('assert'),
exec = require('child_process').exec,
util = require('util'),
fs = require('fs'),
mailer = require('./src/mailer.js');
var assert = require('assert'),
mailer = require('./src/mailer.js'),
safe = require('safetydance'),
supervisor = require('supervisord-eventlistener'),
path = require('path'),
util = require('util');
var gLastNotifyTime = {};
var gCooldownTime = 1000 * 60 * 5; // 5 min
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 logFilePath = util.format('/var/log/supervisor/%s.log', program);
if (!fs.existsSync(logFilePath)) return callback(new Error(util.format('Log file %s does not exist.', logFilePath)));
fs.readFile(logFilePath, 'utf-8', function (error, data) {
if (error) return callback(error);
var lines = data.split('\n');
var boxLogLines = lines.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');
callback(null, result);
});
});
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
}
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) {
+38 -51
View File
@@ -105,8 +105,8 @@
}
},
"cloudron-manifestformat": {
"version": "1.4.0",
"from": "cloudron-manifestformat@1.4.0",
"version": "1.6.0",
"from": "cloudron-manifestformat@1.6.0",
"dependencies": {
"java-packagename-regex": {
"version": "1.0.0",
@@ -131,18 +131,17 @@
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz"
},
"connect-lastmile": {
"version": "0.0.12",
"from": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz",
"version": "0.0.13",
"from": "connect-lastmile@0.0.13",
"dependencies": {
"debug": {
"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",
"dependencies": {
"ms": {
"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"
}
}
@@ -1288,69 +1287,69 @@
},
"dockerode": {
"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",
"dependencies": {
"docker-modem": {
"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",
"dependencies": {
"debug": {
"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"
},
"follow-redirects": {
"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"
},
"JSONStream": {
"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",
"dependencies": {
"jsonparse": {
"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"
},
"through": {
"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"
}
}
},
"querystring": {
"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"
},
"readable-stream": {
"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",
"dependencies": {
"core-util-is": {
"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"
},
"isarray": {
"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"
},
"string_decoder": {
"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"
},
"inherits": {
"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"
}
}
@@ -1691,88 +1690,81 @@
},
"ldapjs": {
"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",
"dependencies": {
"asn1": {
"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"
},
"assert-plus": {
"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"
},
"bunyan": {
"version": "0.22.1",
"from": "bunyan@0.22.1",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
"dependencies": {
"mv": {
"version": "0.0.5",
"from": "mv@0.0.5",
"resolved": "https://registry.npmjs.org/mv/-/mv-0.0.5.tgz"
}
}
"from": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
},
"nopt": {
"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",
"dependencies": {
"abbrev": {
"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"
}
}
},
"pooling": {
"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",
"dependencies": {
"once": {
"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"
},
"vasync": {
"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",
"dependencies": {
"jsprim": {
"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",
"dependencies": {
"extsprintf": {
"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"
},
"json-schema": {
"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"
},
"verror": {
"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"
}
}
},
"verror": {
"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",
"dependencies": {
"extsprintf": {
"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"
}
}
@@ -1780,11 +1772,6 @@
}
}
}
},
"dtrace-provider": {
"version": "0.2.8",
"from": "dtrace-provider@0.2.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz"
}
}
},
@@ -2273,9 +2260,9 @@
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.0.tgz"
},
"safetydance": {
"version": "0.0.16",
"from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz"
"version": "0.0.19",
"from": "safetydance@0.0.19",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
},
"semver": {
"version": "4.3.6",
+6 -6
View File
@@ -18,9 +18,9 @@
"dependencies": {
"async": "^1.2.1",
"body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.4.0",
"cloudron-manifestformat": "^1.6.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.12",
"connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
@@ -55,7 +55,7 @@
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^1.0.0",
"proxy-middleware": "^0.13.0",
"safetydance": "0.0.16",
"safetydance": "0.0.19",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
@@ -92,9 +92,9 @@
"yargs": "^3.15.0"
},
"scripts": {
"migrate_local": "NODE_ENV=local DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "NODE_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test",
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+14 -23
View File
@@ -3,20 +3,19 @@
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${script_dir}/../node_modules/.bin/json"
arg_restore_url=""
arg_restore_key=""
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_box_versions_url=""
arg_fqdn=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire="false"
arg_tls_cert=""
arg_tls_key=""
arg_api_server_origin=""
arg_web_server_origin=""
arg_fqdn=""
arg_token=""
arg_version=""
arg_is_custom_domain="false"
arg_developer_mode=""
arg_retire="false"
arg_model=""
arg_web_server_origin=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}"
@@ -36,18 +35,12 @@ EOF
arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey)
arg_developer_mode=$(echo "$2" | $json developerMode)
[[ "${arg_developer_mode}" == "" ]] && arg_developer_mode="false"
arg_restore_url=$(echo "$2" | $json restoreUrl)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
arg_restore_key=$(echo "$2" | $json restoreKey)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_model=$(echo "$2" | $json model)
[[ "${arg_model}" == "null" ]] && arg_model=""
shift 2
;;
--) break;;
@@ -56,16 +49,14 @@ EOF
done
echo "Parsed arguments:"
echo "restore url: ${arg_restore_url}"
echo "restore key: ${arg_restore_key}"
echo "box versions url: ${arg_box_versions_url}"
echo "api server: ${arg_api_server_origin}"
echo "web server: ${arg_web_server_origin}"
echo "box versions url: ${arg_box_versions_url}"
echo "fqdn: ${arg_fqdn}"
echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "custom domain: ${arg_is_custom_domain}"
echo "restore key: ${arg_restore_key}"
echo "restore url: ${arg_restore_url}"
echo "tls cert: ${arg_tls_cert}"
echo "tls key: ${arg_tls_key}"
echo "developer mode: ${arg_developer_mode}"
echo "model: ${arg_model}"
echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
+12 -9
View File
@@ -1,26 +1,29 @@
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
Defaults!/home/yellowtent/box/src/scripts/backupbox.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupbox.sh
Defaults!/home/yellowtent/box/src/scripts/backupapp.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupapp.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupapp.sh
Defaults!/home/yellowtent/box/src/scripts/restoreapp.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/restoreapp.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restoreapp.sh
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME NODE_ENV"
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME NODE_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
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
@@ -7,4 +7,4 @@ stdout_logfile=/var/log/supervisor/apphealthtask.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
+1 -1
View File
@@ -7,4 +7,4 @@ stdout_logfile=/var/log/supervisor/box.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*,connect-lastmile",NODE_ENV="cloudron"
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*,connect-lastmile",BOX_ENV="cloudron",NODE_ENV="production"
@@ -8,4 +8,4 @@ stderr_logfile=/var/log/supervisor/crashnotifier.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",NODE_ENV="cloudron"
environment=HOME="/home/yellowtent",USER="yellowtent",BOX_ENV="cloudron",NODE_ENV="production"
@@ -7,4 +7,4 @@ stdout_logfile=/var/log/supervisor/janitor.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
@@ -7,4 +7,4 @@ stdout_logfile=/var/log/supervisor/oauthproxy.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
+7 -6
View File
@@ -7,6 +7,7 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly BOX_SRC_DIR="/home/yellowtent/box"
readonly DATA_DIR="/home/yellowtent/data"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION
@@ -14,6 +15,10 @@ echo "Setting up nginx update page"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
@@ -24,14 +29,10 @@ infra_version="none"
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else
# keep this is sync with config.js appFqdn()
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
+4 -6
View File
@@ -19,6 +19,7 @@ source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
readonly is_update=$([[ -d "${DATA_DIR}/box" ]] && echo "true" || echo "false")
@@ -77,7 +78,7 @@ set_progress "25" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
NODE_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
set_progress "28" "Setup collectd"
@@ -95,7 +96,7 @@ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs
# generate these for update code paths as well to overwrite splash
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
@@ -108,7 +109,6 @@ set_progress "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
set_progress "65" "Creating cloudron.conf"
admin_origin="https://${admin_fqdn}"
sudo -u yellowtent -H bash <<EOF
set -eu
echo "Creating cloudron.conf"
@@ -128,9 +128,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
},
"model": "${arg_model}",
"developerMode": ${arg_developer_mode}
}
}
CONF_END
+2 -4
View File
@@ -37,11 +37,9 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
error_page 500 502 503 504 =200 @appstatus;
error_page 500 502 503 504 @appstatus;
location @appstatus {
internal;
root <%= sourceDir %>/webadmin/dist;
rewrite ^/$ /appstatus.html break;
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
}
location / {
+3
View File
@@ -17,6 +17,9 @@ http {
'"$request" $status $body_bytes_sent $request_time '
'"$http_referer" "$http_user_agent"';
# required for long host names
server_names_hash_bucket_size 128;
access_log access.log combined2;
sendfile on;
+2 -2
View File
@@ -660,11 +660,11 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
get(appId, function (error, app) {
if (!canAutoupdateApp(app, updateInfo.manifest)) {
if (!canAutoupdateApp(app, updateInfo[appId].manifest)) {
return iteratorDone();
}
update(appId, updateInfo.manifest, app.portBindings, null /* icon */, function (error) {
update(appId, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
if (error) debug('Error initiating autoupdate of %s', appId);
iteratorDone(null);
+1 -2
View File
@@ -93,7 +93,7 @@ function configureNginx(app, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debugApp(app, 'writing config to %s', nginxConfigFilename);
@@ -189,7 +189,6 @@ function createContainer(app, callback) {
}
env.push('CLOUDRON=1');
env.push('ADMIN_ORIGIN' + '=' + config.adminOrigin()); // ## remove
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
env.push('API_ORIGIN' + '=' + config.adminOrigin());
+70 -50
View File
@@ -19,8 +19,7 @@ exports = module.exports = {
reboot: reboot,
migrate: migrate,
backup: backup,
ensureBackup: ensureBackup
};
ensureBackup: ensureBackup};
var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
@@ -108,7 +107,7 @@ CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.NODE_ENV !== 'test') {
if (process.env.BOX_ENV !== 'test') {
addMailDnsRecords();
}
@@ -231,6 +230,7 @@ function getCloudronDetails(callback) {
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
// TODO avoid pyramid of awesomeness with async
getCloudronDetails(function (error, result) {
if (error) {
console.error('Failed to fetch cloudron details.', error);
@@ -245,20 +245,24 @@ function getConfig(callback) {
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: sysinfo.getIp(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: config.developerMode(),
region: result.region,
size: result.size,
cloudronName: cloudronName
settings.getDeveloperMode(function (error, developerMode) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: sysinfo.getIp(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
cloudronName: cloudronName
});
});
});
});
@@ -266,13 +270,12 @@ function getConfig(callback) {
function sendHeartbeat() {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
debug('Sending heartbeat ' + url);
// TODO: this must be a POST
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
if (error) debug('Error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat successful');
else debug('Heartbeat sent to %s', url);
});
}
@@ -413,16 +416,22 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
progress.set(progress.UPDATE, 0, 'Begin ' + (boxUpdateInfo.update ? 'upgrade': 'update'));
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
locker.unlock(locker.OP_BOX_UPDATE);
if (error) {
debug('Upgrade failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
} else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
locker.unlock(locker.OP_BOX_UPDATE);
if (error) {
debug('Update failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
}
@@ -432,17 +441,22 @@ function update(boxUpdateInfo, callback) {
function doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
progress.set(progress.UPDATE, 5, 'Create app and box backup');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
backupBoxAndApps(function (error) {
if (error) return callback(error);
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.end(function (error, result) {
if (error) return callback(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return callback(new Error('Server not ready to upgrade: ' + result.body));
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
progress.set(progress.UPDATE, 10, 'Updating base system');
@@ -456,45 +470,49 @@ function doUpgrade(boxUpdateInfo, callback) {
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
progress.set(progress.UPDATE, 5, 'Create box backup');
function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create box backup for update');
backupBox(function (error) {
if (error) return callback(error);
if (error) return updateError(error);
// fetch a signed sourceTarballUrl
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
.end(function (error, result) {
if (error) return callback(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return callback(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return callback(new Error('Error fetching sourceTarballUrl response: ' + result.body));
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = {
sourceTarballUrl: result.body.url,
// this data is opaque to the installer
// IMPORTANT: if you change this, fix up argparser.sh as well. keep these sorted for readability
data: {
boxVersionsUrl: config.get('boxVersionsUrl'),
version: boxUpdateInfo.version,
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
boxVersionsUrl: config.get('boxVersionsUrl'),
fqdn: config.fqdn(),
token: config.token(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
restoreUrl: null,
restoreKey: null,
developerMode: config.developerMode() // this survives updates but not upgrades
restoreUrl: null,
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
token: config.token(),
version: boxUpdateInfo.version,
webServerOrigin: config.webServerOrigin()
}
};
debug('updating box %j', args);
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error) return callback(error);
if (result.status !== 202) return callback(new Error('Error initiating update: ' + result.body));
if (error) return updateError(error);
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
@@ -545,7 +563,7 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError.INTERNAL_ERROR, error);
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: url %s', result.url);
@@ -569,7 +587,7 @@ function backupBox(callback) {
if (error) return callback(new CloudronError(CloudronError.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
appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupBoxWithAppBackupIds(appBackupIds, callback);
});
@@ -591,25 +609,27 @@ function backupBoxAndApps(callback) {
++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, app.location);
progress.set(progress.BACKUP, step * processed, 'Backing up app at ' + app.location);
if (error && error.reason === AppsError.BAD_STATE) {
debugApp(app, 'Skipping backup (istate:%s health%s). Reusing %s', app.installationState, app.health, app.lastBackupId);
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
backupId = app.lastBackupId;
}
return iteratorCallback(null, backupId);
});
}, function appsBackedUp(error, backupIds) {
if (error) return callback(error);
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, '');
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, restoreKey);
});
});
});
}
+2 -9
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
set: set,
// ifdefs to check environment
CLOUDRON: process.env.NODE_ENV === 'cloudron',
TEST: process.env.NODE_ENV === 'test',
CLOUDRON: process.env.BOX_ENV === 'cloudron',
TEST: process.env.BOX_ENV === 'test',
// convenience getters
apiServerOrigin: apiServerOrigin,
@@ -22,7 +22,6 @@ exports = module.exports = {
version: version,
isCustomDomain: isCustomDomain,
database: database,
developerMode: developerMode,
// these values are derived
adminOrigin: adminOrigin,
@@ -76,7 +75,6 @@ function initConfig() {
data.port = 3000;
data.apiServerOrigin = null;
data.database = null;
data.developerMode = false;
} else if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
@@ -88,7 +86,6 @@ function initConfig() {
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
data.developerMode = false;
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -171,10 +168,6 @@ function database() {
return get('database');
}
function developerMode() {
return get('developerMode');
}
function isDev() {
return /dev/i.test(get('boxVersionsUrl'));
}
+5 -2
View File
@@ -21,7 +21,7 @@ var gAutoupdaterJob = null,
var gInitialized = false;
var NOOP_CALLBACK = function (error) { console.error(error); };
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
// cron format
// Seconds: 0-59
@@ -98,12 +98,15 @@ function autoupdatePatternChanged(pattern) {
gAutoupdaterJob = new CronJob({
cronTime: pattern,
onTick: function() {
debug('Starting autoupdate');
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.update(updateInfo.box, NOOP_CALLBACK);
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
} else {
debug('No auto updates available');
}
},
start: true,
+25 -6
View File
@@ -7,12 +7,15 @@ exports = module.exports = {
enabled: enabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
};
var assert = require('assert'),
tokendb = require('./tokendb.js'),
config = require('./config.js'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -39,16 +42,20 @@ DeveloperError.INTERNAL_ERROR = 'Internal Error';
function enabled(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null, config.developerMode());
settings.getDeveloperMode(function (error, enabled) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
callback(null, enabled);
});
}
function setEnabled(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
config.set('developerMode', enabled);
callback(null);
settings.setDeveloperMode(enabled, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
callback(null);
});
}
function issueDeveloperToken(user, callback) {
@@ -64,3 +71,15 @@ function issueDeveloperToken(user, callback) {
callback(null, { token: token, expiresAt: expiresAt });
});
}
function getNonApprovedApps(callback) {
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
if (result.status !== 200) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);
});
}
+1 -1
View File
@@ -10,7 +10,7 @@ exports = module.exports = (function () {
var docker;
var options = connectOptions(); // the real docker
if (process.env.NODE_ENV === 'test') {
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 });
} else {
+3 -1
View File
@@ -43,11 +43,13 @@ function start(callback) {
dn: dn.toString(),
attributes: {
objectclass: ['user'],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
mail: entry.email,
displayname: entry.username,
username: entry.username
username: entry.username,
samaccountname: entry.username // to support ActiveDirectory clients
}
};
+1 -1
View File
@@ -50,6 +50,6 @@ Locker.prototype.unlock = function (operation) {
this.emit('unlocked', operation);
return null;
}
};
exports = module.exports = new Locker();
+15
View File
@@ -0,0 +1,15 @@
<%if (format === 'text') { %>
New <%= type %> from <%= fqdn %>.
Sender: <%= user.email %>
Sent at: <%= new Date().toUTCString() %>
Subject: <%= subject %>
-----------------------------------------------------------
<%= description %>
<% } else { %>
<% } %>
+24 -1
View File
@@ -15,7 +15,12 @@ exports = module.exports = {
sendCrashNotification: sendCrashNotification,
appDied: appDied
appDied: appDied,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
sendFeedback: sendFeedback
};
var assert = require('assert'),
@@ -277,3 +282,21 @@ function sendCrashNotification(program, context) {
enqueue(mailOptions);
}
function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
var mailOptions = {
from: config.get('adminEmail'),
to: 'support@cloudron.io',
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
};
enqueue(mailOptions);
}
+1
View File
@@ -21,6 +21,7 @@ var progress = {
backup: null
};
// We use -1 for percentage to indicate errors
function set(tag, percent, message) {
assert(tag === exports.UPDATE || tag === exports.BACKUP);
assert.strictEqual(typeof percent, 'number');
+1 -1
View File
@@ -117,7 +117,7 @@ function installApp(req, res, next) {
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
// allow tests to provide an appId for testing
var appId = (process.env.NODE_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
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 restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
+15 -1
View File
@@ -11,13 +11,15 @@ exports = module.exports = {
getConfig: getConfig,
update: update,
migrate: migrate,
setCertificate: setCertificate
setCertificate: setCertificate,
feedback: feedback
};
var assert = require('assert'),
cloudron = require('../cloudron.js'),
config = require('../config.js'),
progress = require('../progress.js'),
mailer = require('../mailer.js'),
CloudronError = cloudron.CloudronError,
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
@@ -157,3 +159,15 @@ function setCertificate(req, res, next) {
next(new HttpSuccess(202, {}));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) 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.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description);
next(new HttpSuccess(201, {}));
}
+9 -1
View File
@@ -6,7 +6,8 @@ exports = module.exports = {
enabled: enabled,
setEnabled: setEnabled,
status: status,
login: login
login: login,
apps: apps
};
var developer = require('../developer.js'),
@@ -46,3 +47,10 @@ function login(req, res, next) {
});
})(req, res, next);
}
function apps(req, res, next) {
developer.getNonApprovedApps(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { apps: result }));
});
}
+3
View File
@@ -76,6 +76,9 @@ function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(new HttpError(500, error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', 'image/png');
res.status(200).send(avatar);
});
+24 -20
View File
@@ -30,6 +30,7 @@ var appdb = require('../../appdb.js'),
request = require('superagent'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
sysinfo = require('../../sysinfo.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
@@ -431,28 +432,30 @@ describe('App API', function () {
it('app install succeeds without password but developer token', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
config.set('developerMode', true);
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
// overwrite non dev token
token = result.body.token;
// overwrite non dev token
token = result.body.token;
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
expect(fake.isDone()).to.be.ok();
APP_ID = res.body.id;
done(err);
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
expect(fake.isDone()).to.be.ok();
APP_ID = res.body.id;
done(err);
});
});
});
});
@@ -570,7 +573,8 @@ describe('App installation', function () {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
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');
clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
expect(error).to.not.be.ok();
+1 -1
View File
@@ -50,7 +50,7 @@ function setup(done) {
},
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
}
], done);
+305 -305
View File
@@ -15,7 +15,8 @@ var async = require('async'),
nock = require('nock'),
hat = require('hat'),
superagent = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -62,137 +63,129 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
config.set('developerMode', true);
describe('without developer mode', function () {
before(function (done) {
settings.setDeveloperMode(false, done);
});
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
it('fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
it('fails if not in developerMode', function (done) {
config.set('developerMode', false);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
describe('with developer mode', function () {
before(function (done) {
settings.setDeveloperMode(true, done);
});
});
it('fails without appId', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
});
it('fails with empty appId', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails without appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('fails without scope', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails with empty appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('fails with empty scope', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails without scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('fails without redirectURI', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails with empty scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('fails with empty redirectURI', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails without redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('fails with malformed redirectURI', function (done) {
config.set('developerMode', true);
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
it('fails with empty redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('succeeds', function (done) {
config.set('developerMode', true);
it('fails with malformed redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
done();
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
done();
});
});
});
});
@@ -230,9 +223,9 @@ describe('OAuth Clients API', function () {
});
},
function (callback) {
config.set('developerMode', true);
settings.setDeveloperMode.bind(null, true),
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
@@ -250,51 +243,56 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
config.set('developerMode', true);
describe('without developer mode', function () {
before(function (done) {
settings.setDeveloperMode(false, done);
});
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
it('fails', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
it('fails if not in developerMode', function (done) {
config.set('developerMode', false);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
describe('with developer mode', function () {
before(function (done) {
settings.setDeveloperMode(true, done);
});
});
it('fails with unknown id', function (done) {
config.set('developerMode', true);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
});
it('succeeds', function (done) {
config.set('developerMode', true);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
it('fails with unknown id', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
});
});
});
});
@@ -339,9 +337,9 @@ describe('OAuth Clients API', function () {
});
},
function (callback) {
config.set('developerMode', true);
settings.setDeveloperMode.bind(null, true),
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
@@ -359,116 +357,113 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
describe('without developer mode', function () {
before(function (done) {
settings.setDeveloperMode(false, done);
});
});
it('fails if not in developerMode', function (done) {
config.set('developerMode', false);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
it('fails without appId', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty appId', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: '', redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without redirectURI', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty redirectURI', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with malformed redirectURI', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: 'foobar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
config.set('developerMode', true);
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
it('fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.appId).to.equal(CLIENT_1.appId);
expect(result.body.redirectURI).to.equal(CLIENT_1.redirectURI);
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
});
describe('with developer mode', function () {
before(function (done) {
settings.setDeveloperMode(true, done);
});
it('fails without token', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without appId', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty appId', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: '', redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with malformed redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: 'foobar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.appId).to.equal(CLIENT_1.appId);
expect(result.body.redirectURI).to.equal(CLIENT_1.redirectURI);
done();
});
});
});
});
});
@@ -506,9 +501,9 @@ describe('OAuth Clients API', function () {
});
},
function (callback) {
config.set('developerMode', true);
settings.setDeveloperMode.bind(null, true),
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
@@ -526,58 +521,63 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
config.set('developerMode', true);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
describe('without developer mode', function () {
before(function (done) {
settings.setDeveloperMode(false, done);
});
});
it('fails if not in developerMode', function (done) {
config.set('developerMode', false);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
it('fails with unknown id', function (done) {
config.set('developerMode', true);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
config.set('developerMode', true);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
it('fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(404);
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
});
describe('with developer mode', function () {
before(function (done) {
settings.setDeveloperMode(true, done);
});
it('fails without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown id', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(404);
done();
});
});
});
});
});
+153
View File
@@ -26,6 +26,7 @@ var token = null; // authentication token
var server;
function setup(done) {
nock.cleanAll();
config.set('version', '0.5.0');
server.start(done);
}
@@ -501,6 +502,158 @@ describe('Cloudron', function () {
});
});
});
describe('feedback', function () {
before(function (done) {
async.series([
setup,
function (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, {});
config._reset();
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
after(cleanup);
it('fails without token', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ 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(400);
done();
});
});
it('fails with empty type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: '', 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(400);
done();
});
});
it('fails with unknown type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'foobar', 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(400);
done();
});
});
it('succeeds with ticket type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', 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('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) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with feedback type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'feedback', 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 subject', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
});
+30 -23
View File
@@ -12,7 +12,8 @@ var async = require('async'),
expect = require('expect.js'),
nock = require('nock'),
request = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -63,37 +64,43 @@ describe('Developer API', function () {
after(cleanup);
it('fails without token', function (done) {
config.set('developerMode', true);
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
request.get(SERVER_URL + '/api/v1/developer')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
request.get(SERVER_URL + '/api/v1/developer')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
});
it('succeeds (enabled)', function (done) {
config.set('developerMode', true);
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
done();
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
done();
});
});
});
it('succeeds (not enabled)', function (done) {
config.set('developerMode', false);
settings.setDeveloperMode(false, function (error) {
expect(error).to.be(null);
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
});
@@ -231,11 +238,11 @@ describe('Developer API', function () {
describe('login', function () {
before(function (done) {
config.set('developerMode', true);
async.series([
setup,
settings.setDeveloperMode.bind(null, true),
function (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, {});
+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
@@ -17,7 +17,7 @@ if [[ "$1" == "--check" ]]; then
exit 0
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/data/$1"
btrfs subvolume create "${app_data_dir}"
mkdir -p "${app_data_dir}/data"
+1 -1
View File
@@ -12,7 +12,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
shutdown -r now
fi
+1 -1
View File
@@ -12,7 +12,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
/etc/init.d/collectd restart
fi
+1 -1
View File
@@ -17,7 +17,7 @@ if [[ "${OSTYPE}" == "darwin"* ]]; then
export PATH=$PATH:/usr/local/bin
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
nginx -s reload
fi
+1 -1
View File
@@ -17,7 +17,7 @@ if [[ "$1" == "--check" ]]; then
exit 0
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/data/$1"
if [[ -d "${app_data_dir}" ]]; then
find "${app_data_dir}" -mindepth 1 -delete
+5 -1
View File
@@ -43,7 +43,7 @@ function initializeExpressSync() {
app.set('view options', { layout: true, debug: true });
app.set('view engine', 'ejs');
if (process.env.NODE_ENV === 'test') {
if (process.env.BOX_ENV === 'test') {
app.use(express.static(path.join(__dirname, '/../webadmin')));
} else {
app.use(middleware.morgan('dev', { immediate: false }));
@@ -92,6 +92,7 @@ function initializeExpressSync() {
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
// private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
@@ -101,6 +102,9 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
router.get ('/api/v1/profile', profileScope, routes.user.profile);
router.get ('/api/v1/users', usersScope, routes.user.list);
+31
View File
@@ -17,12 +17,16 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getDeveloperMode: getDeveloperMode,
setDeveloperMode: setDeveloperMode,
getDefaultSync: getDefaultSync,
getAll: getAll,
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode',
events: new (require('events').EventEmitter)()
};
@@ -45,6 +49,7 @@ var gDefaults = (function () {
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = tz;
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
return result;
})();
@@ -179,6 +184,32 @@ function setCloudronAvatar(avatar, callback) {
return callback(null);
}
function getDeveloperMode(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.DEVELOPER_MODE_KEY, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DEVELOPER_MODE_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
// settingsdb holds string values only
callback(null, !!enabled);
});
}
function setDeveloperMode(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
// settingsdb takes string values only
settingsdb.set(exports.DEVELOPER_MODE_KEY, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.DEVELOPER_MODE_KEY, enabled);
return callback(null);
});
}
function getDefaultSync(name) {
assert.strictEqual(typeof name, 'string');
+1 -1
View File
@@ -54,7 +54,7 @@ function uninitialize(callback) {
function startNextTask() {
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());
}
+2 -1
View File
@@ -16,6 +16,7 @@ scripts=("${SOURCE_DIR}/scripts/rmappdir.sh" \
"${SOURCE_DIR}/scripts/restoreapp.sh" \
"${SOURCE_DIR}/scripts/reboot.sh" \
"${SOURCE_DIR}/scripts/backupswap.sh" \
"${SOURCE_DIR}/scripts/collectlogs.sh" \
"${SOURCE_DIR}/scripts/reloadcollectd.sh")
for script in "${scripts[@]}"; do
@@ -24,7 +25,7 @@ for script in "${scripts[@]}"; do
echo "${script} does not have sudo access."
echo "You have to add the lines below to /etc/sudoers.d/yellowtent."
echo ""
echo "Defaults!${script} env_keep=\"HOME NODE_ENV\""
echo "Defaults!${script} env_keep=\"HOME BOX_ENV\""
echo "${USER} ALL=(ALL) NOPASSWD: ${script}"
echo ""
exit 1
+27
View File
@@ -33,6 +33,7 @@ describe('Settings', function () {
done();
});
});
it('can get default autoupdate_pattern', function (done) {
settings.getAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
@@ -40,6 +41,7 @@ describe('Settings', function () {
done();
});
});
it ('can get default cloudron name', function (done) {
settings.getCloudronName(function (error, name) {
expect(error).to.be(null);
@@ -47,6 +49,7 @@ describe('Settings', function () {
done();
});
});
it('can get default cloudron avatar', function (done) {
settings.getCloudronAvatar(function (error, gravatar) {
expect(error).to.be(null);
@@ -54,6 +57,30 @@ describe('Settings', function () {
done();
});
});
it('can get default developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(false);
done();
});
});
it('can set developer mode', function (done) {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(true);
done();
});
});
it('can get all values', function (done) {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
+5 -11
View File
@@ -30,20 +30,14 @@
// create main application module
var app = angular.module('Application', []);
// FIXME this does not work with custom domains!
function detectApiOrigin() {
var host = window.location.host;
var tmp = host.split('.')[0];
if (tmp.indexOf('-') === -1) return 'https://my-' + host;
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
}
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.apiOrigin = detectApiOrigin();
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.cloudronName = 'Cloudron';
$scope.referrer = search.referrer || null;
// try to fetch cloudron status
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' App Error';
@@ -64,7 +58,7 @@
<h1> {{cloudronName}} </h1>
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
This app is currently not running. Please retry later.
This app is currently not running. <a href="{{ referrer }}">Please retry later</a>.
<footer>
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span>
+3 -12
View File
@@ -30,22 +30,13 @@
// create main application module
var app = angular.module('Application', []);
// FIXME this does not work with custom domains!
function detectApiOrigin() {
var host = window.location.host;
var tmp = host.split('.')[0];
if (tmp.indexOf('-') === -1) return 'https://my-' + host;
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
}
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.apiOrigin = detectApiOrigin();
$scope.cloudronName = 'Cloudron';
$scope.webServerOriginLink = '/';
$scope.errorMessage = '';
// try to fetch at least config.json to get appstore url
$http.get($scope.apiOrigin + '/config.json').success(function(data, status) {
$http.get('/config.json').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.webServerOriginLink = data.webServerOrigin + '/console.html';
}).error(function (data, status) {
@@ -54,7 +45,7 @@
});
// try to fetch cloudron status
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' Error';
@@ -76,7 +67,7 @@
<div class="wrapper">
<div class="content">
<img src="/img/logo_inverted_192.png"/>
<img src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1>
<div ng-show="errorCode == 0">
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+5 -3
View File
@@ -6,7 +6,7 @@
<title> Cloudron </title>
<link href="/img/favicon.png" rel="icon" type="image/png">
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
@@ -117,6 +117,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="index.html">{{config.cloudronName || 'Cloudron'}}</a>
</div>
<!-- /.navbar-header -->
@@ -142,9 +143,10 @@
<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 && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
<li ng-show="user.admin"><a href="#/upgrade"><i class="fa fa-arrow-up fa-fw"></i> Upgrade</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</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="#/upgrade"><i class="fa fa-arrow-up fa-fw"></i> Upgrade</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
</ul>
+23 -1
View File
@@ -32,7 +32,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
return callback(new ClientError(status, data));
}
if (result.update) window.location.href = '/update.html';
if (result.update && result.update.percent !== -1) window.location.href = '/update.html';
else callback(new ClientError(status, data));
}, function (data, status) {
client.error(data);
@@ -324,6 +324,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getNonApprovedApps = function (callback) {
if (!this._config.developerMode) return callback(null, []);
$http.get(client.apiOrigin + '/api/v1/developer/apps').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.apps || []);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getApp = function (appId, callback) {
var appFound = null;
this._installedApps.some(function (app) {
@@ -460,6 +469,19 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.feedback = function (type, subject, description, callback) {
var data = {
type: type,
subject: subject,
description: description
};
$http.post(client.apiOrigin + '/api/v1/cloudron/feedback', data).success(function (data, status) {
if (status !== 201) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.createUser = function (username, email, callback) {
var data = {
username: username,
+3
View File
@@ -37,6 +37,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
}).when('/upgrade', {
controller: 'UpgradeController',
templateUrl: 'views/upgrade.html'
+2 -4
View File
@@ -86,9 +86,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (error && error.statusCode === 401) return $scope.login();
if (error) return $scope.error(error);
// check if we are actually updateing
if (Client.getConfig().progress.update) window.location.href = '/update.html';
Client.refreshUserInfo(function (error, result) {
if (error) return $scope.error(error);
@@ -122,7 +119,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
// wait till the view has loaded until showing a modal dialog
Client.onConfig(function (config) {
if (config.progress.update) {
// check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
+45 -15
View File
@@ -28,11 +28,8 @@ app.config(['$routeProvider', function ($routeProvider) {
controller: 'StepController',
templateUrl: 'views/setup/step2.html'
}).when('/step3', {
controller: 'StepController',
templateUrl: 'views/setup/step3.html'
}).when('/step4', {
controller: 'FinishController',
templateUrl: 'views/setup/step4.html'
templateUrl: 'views/setup/step3.html'
}).otherwise({ redirectTo: '/'});
}]);
@@ -51,27 +48,51 @@ app.service('Wizard', [ function () {
}, {
file: null,
data: null,
url: '/img/avatars/cloudfacegreen.png'
url: '/img/avatars/rubber-duck.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudfaceturquoise.png'
url: '/img/avatars/carrot.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesgreen.png'
url: '/img/avatars/cup.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassespink.png'
url: '/img/avatars/football.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesturquoise.png'
url: '/img/avatars/owl.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesyellow.png'
url: '/img/avatars/space-rocket.png'
}, {
file: null,
data: null,
url: '/img/avatars/armchair.png'
}, {
file: null,
data: null,
url: '/img/avatars/cap.png'
}, {
file: null,
data: null,
url: '/img/avatars/pan.png'
}, {
file: null,
data: null,
url: '/img/avatars/meat.png'
}, {
file: null,
data: null,
url: '/img/avatars/umbrella.png'
}, {
file: null,
data: null,
url: '/img/avatars/jar.png'
}];
this.avatar = {};
this.avatarBlob = null;
@@ -82,8 +103,9 @@ app.service('Wizard', [ function () {
this.avatar = avatar;
// scale image and get the blob now
var img = document.getElementById('previewAvatar');
// scale image and get the blob now. do not use the previewAvatar element here because it is not updated yet
var img = document.createElement('img');
img.src = avatar.data || avatar.url;
var canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
@@ -122,7 +144,7 @@ app.service('Wizard', [ function () {
return instance;
}]);
app.controller('StepController', ['$scope', '$location', 'Wizard', function ($scope, $location, Wizard) {
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard;
$scope.next = function (page, bad) {
@@ -143,7 +165,7 @@ app.controller('StepController', ['$scope', '$location', 'Wizard', function ($sc
};
// cheap way to detect if we are in avatar and name selection step
if ($('#previewAvatar').get(0) && $('#avatarFileInput').get(0)) {
if ($route.current.templateUrl === 'views/setup/step1.html') {
$('#avatarFileInput').get(0).onchange = function (event) {
var fr = new FileReader();
fr.onload = function () {
@@ -161,8 +183,16 @@ app.controller('StepController', ['$scope', '$location', 'Wizard', function ($sc
fr.readAsDataURL(event.target.files[0]);
};
$scope.wizard.setPreviewAvatar($scope.wizard.availableAvatars[0]);
// ensure image got loaded before setting the preview avatar
var image = document.createElement('img');
var randomIndex = Math.floor(Math.random() * $scope.wizard.availableAvatars.length);
image.onload = function() {
$scope.$apply(function () { $scope.wizard.setPreviewAvatar($scope.wizard.availableAvatars[randomIndex]); });
image = null;
};
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
}
}]);
app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) {
+16 -7
View File
@@ -4,20 +4,29 @@
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', '$interval', function ($scope, $http, $interval) {
var apiOrigin = '';
$scope.title = 'Update in progress...';
$scope.percent = 0;
$scope.message = '';
$scope.error = false;
function loadWebadmin() {
$scope.loadWebadmin = function () {
window.location.href = '/';
}
};
function fetchProgress() {
$http.get(apiOrigin + '/api/v1/cloudron/progress').success(function(data, status) {
$http.get('/api/v1/cloudron/progress').success(function(data, status) {
if (status === 404) return; // just wait until we create the progress.json on the server side
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
if (data.update === null) return loadWebadmin();
if (data.update === null) return $scope.loadWebadmin();
$('#updateProgressBar').css('width', data.update.percent + '%');
$('#updateProgressMessage').html(data.update.message);
if (data.update.percent === -1) {
$scope.title = 'Update Error';
$scope.error = true;
$scope.message = data.update.message;
} else {
$scope.percent = data.update.percent;
$scope.message = data.update.message;
}
}).error(function (data, status) {
console.error(status, data);
});
+2 -1
View File
@@ -40,6 +40,7 @@
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.apiOrigin = detectApiOrigin();
$scope.cloudronAvatar = $scope.apiOrigin + '/api/v1/cloudron/avatar';
$scope.cloudronName = 'Cloudron';
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
@@ -58,7 +59,7 @@
<div class="wrapper">
<div class="content">
<img src="/img/logo_inverted_192.png"/>
<img ng-src="{{ cloudronAvatar || '/img/logo_inverted_192.png' }}" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1>
<p>
There is no app configured for this domain. If you want to put an app at this location,<br/>
+84 -8
View File
@@ -92,7 +92,6 @@ html {
}
.highlight {
transition: background-color 500ms;
}
.highlight:hover {
@@ -104,6 +103,16 @@ html {
margin: 0 auto;
}
.navbar-brand-icon {
padding: 5px 15px;
}
.navbar-nav > li > a {
@media(min-width:768px) {
padding: 13px 15px;
}
}
// ----------------------------
// Apps view
// ----------------------------
@@ -147,6 +156,18 @@ html {
cursor: pointer;
}
.appstore-item-badge-testing {
position: absolute;
right: 15px;
top: 15px;
}
.appstore-item-content-testing {
background-color: #E6E6E6;
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 64px 64px;
}
.appstore-item-content-title {
text-overflow: ellipsis;
white-space: nowrap;
@@ -185,6 +206,31 @@ html {
font-size: 18px;
}
.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 {
max-height: 250px;
overflow-x: none;
@@ -195,6 +241,16 @@ html {
padding-top: 0;
}
.appstore-install-title {
display: inline-block;
margin: 5px 10px;
}
.appstore-install-meta {
margin-left: 10px;
color: $text-muted;
}
.appstore-category-link:hover,
.appstore-category-link:focus,
.appstore-category-link.category-active {
@@ -232,6 +288,10 @@ html {
background-color: #5CB85C;
}
.badge-danger {
background-color: $brand-danger;
}
.progress {
margin-bottom: 0;
}
@@ -269,6 +329,10 @@ html {
color: #5CB85C;
}
.text-bold {
font-weight: bold;
}
.text-large {
font-size: $font-size-h1;
}
@@ -308,12 +372,16 @@ html {
}
.app-icon {
min-height: 64px;
max-height: 64px;
min-width: 64px;
min-height: 80px;
max-height: 80px;
min-width: 80px;
max-width: 100%;
}
.appstore-install .app-icon {
float: left;
}
// ----------------------------
// Animations
// ----------------------------
@@ -675,10 +743,6 @@ $graphs-success-alt: lighten(#27CE65, 20%);
bottom: 0;
width: 100%;
}
h1 {
margin-top: 0;
}
}
@@ -744,3 +808,15 @@ $graphs-success-alt: lighten(#27CE65, 20%);
}
}
}
// ----------------------------
// Support
// ----------------------------
.support {
h3 {
margin-top: 0;
margin-bottom: 20px;
}
}
+10 -4
View File
@@ -33,13 +33,19 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="updateProgressModalLabel">Update in progress...</h4>
<h4 class="modal-title">{{title}}</h4>
</div>
<div class="modal-body">
<div class="modal-body" ng-show="!error">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" id="updateProgressBar" style="width: 0%"></div>
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{percent}}%"></div>
</div>
<span id="updateProgressMessage"></span>
<span>{{message}}</span>
</div>
<div class="modal-body" ng-show="error">
<span>{{message}}</span>
</div>
<div class="modal-footer" ng-show="error">
<button type="button" class="btn btn-primary" ng-click="loadWebadmin()">Ok</button>
</div>
</div>
</div>
+1
View File
@@ -43,6 +43,7 @@
<option value="roleUser">Visible only to Cloudron users</option>
</select>
</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/>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
+1 -1
View File
@@ -214,7 +214,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
AppStore.getManifest(app.appStoreId, function (error, manifest) {
if (error) return console.error(error);
$scope.appUpdate.manifest = manifest;
$scope.appUpdate.manifest = angular.copy(manifest);
// ensure we always operate on objects here
app.portBindings = app.portBindings || {};
+31 -7
View File
@@ -1,13 +1,15 @@
<!-- Modal install app -->
<div class="modal fade" id="appInstallModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="appInstallModalLabel">
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
{{ appInstall.app.manifest.title }}
</h3>
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
<br/>
<span class="appstore-install-meta">{{ appInstall.app.manifest.author }}</span>
<br/>
<span class="appstore-install-meta">{{ appInstall.app.manifest.version }}</span>
</div>
<div class="modal-body">
<div class="collapse" id="collapseInstallForm" data-toggle="false">
@@ -94,11 +96,32 @@
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'sync' }" category="sync">Media Sync</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'git' }" category="git">Code Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki">Wiki</a>
<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>
<br/>
<br/>
<br/>
<div class="appstore-category-missing shadow">
<div ng-show="!feedback.success">
<form name="feedbackForm" ng-submit="submitFeedback()">
<p>
Missing an app? Let us know.
</p>
<textarea class="form-control" id="feedbackDescriptionTextarea" cols="3" ng-model="feedback.description" ng-minlength="1" required placeholder="Name, Category, Links ..."></textarea>
<div ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</div>
<button class="btn btn-success" type="submit" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-fw fa-paper-plane"></i> Submit</button>
</form>
</div>
<div ng-show="feedback.success" class="text-success text-bold">
Thank You!
</div>
</div>
</div>
<div class="col-md-10" ng-show="ready && apps.length">
<div class="row-no-margin">
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
<div class="appstore-item-content highlight" ng-click="showInstall(app)">
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': app.publishState === 'testing' }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
<div class="appstore-item-content-icon col-same-height">
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
</div>
@@ -112,7 +135,8 @@
</div>
</div>
<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="activateFeedbackForm()"><h3>Let us know if you miss something.</h3></a>
</div>
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="!ready">
<h2><i class="fa fa-spinner fa-pulse"></i> Loading</h2>
+69 -6
View File
@@ -20,6 +20,67 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
mediaLinks: []
};
$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;
resetFeedback();
}
$scope.feedback.busy = false;
});
};
$scope.activateFeedbackForm = function () {
$('#feedbackDescriptionTextarea').focus();
};
function getAppList(callback) {
AppStore.getApps(function (error, apps) {
if (error) return callback(error);
// ensure we have a tags property for further use
apps.forEach(function (app) {
if (!app.manifest.tags) app.manifest.tags = [];
});
Client.getNonApprovedApps(function (error, result) {
if (error) return callback(error);
// add testing tag to the manifest for UI and search reasons
result.forEach(function (app) {
if (!app.manifest.tags) app.manifest.tags = [];
app.manifest.tags.push('testing');
});
callback(null, apps.concat(result));
});
});
}
// TODO does not support testing apps in search
$scope.search = function () {
if (!$scope.searchString) return $scope.showCategory(null, $scope.cachedCategory);
@@ -49,7 +110,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.ready = false;
AppStore.getApps(function (error, apps) {
getAppList(function (error, apps) {
if (error) return $timeout($scope.showCategory.bind(null, event), 1000);
if (!$scope.category) {
@@ -105,8 +166,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.portBindings[env] = $scope.appInstall.app.manifest.tcpPorts[env].defaultValue || 0;
$scope.appInstall.portBindingsEnabled[env] = true;
}
};
$scope.doInstall = function () {
@@ -156,7 +215,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
function refresh() {
$scope.ready = false;
AppStore.getApps(function (error, apps) {
getAppList(function (error, apps) {
if (error) {
console.error(error);
return $timeout(refresh, 1000);
@@ -166,8 +225,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// show install app dialog immediately if an app id was passed in the query
if ($routeParams.appId) {
var found = apps.filter(function (app) { return (app.id === $routeParams.appId); });
if (found.length) $scope.showInstall(found[0]);
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]);
}
}
$scope.ready = true;
+35 -10
View File
@@ -20,7 +20,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
};
$scope.createBackup = {
busy: false
busy: false,
percent: 100
};
$scope.nameChange = {
@@ -40,27 +41,51 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}, {
file: null,
data: null,
url: '/img/avatars/cloudfacegreen.png'
url: '/img/avatars/rubber-duck.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudfaceturquoise.png'
url: '/img/avatars/carrot.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesgreen.png'
url: '/img/avatars/cup.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassespink.png'
url: '/img/avatars/football.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesturquoise.png'
url: '/img/avatars/owl.png'
}, {
file: null,
data: null,
url: '/img/avatars/cloudglassesyellow.png'
url: '/img/avatars/space-rocket.png'
}, {
file: null,
data: null,
url: '/img/avatars/armchair.png'
}, {
file: null,
data: null,
url: '/img/avatars/cap.png'
}, {
file: null,
data: null,
url: '/img/avatars/pan.png'
}, {
file: null,
data: null,
url: '/img/avatars/meat.png'
}, {
file: null,
data: null,
url: '/img/avatars/umbrella.png'
}, {
file: null,
data: null,
url: '/img/avatars/jar.png'
}]
};
@@ -192,9 +217,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
if (error) {
console.error('Unable to change developer mode.', error);
} else {
$scope.avatar = $scope.avatarChange.avatar;
avatarChangeReset();
$('#avatarChangeModal').modal('hide');
// Do soft reload, since the browser will not update the avatar URLs in the UI
window.location.reload();
}
$scope.avatarChange.busy = false;
@@ -219,6 +243,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
// check if we are done
if (!data.backup || data.backup.percent >= 100) {
if (data.backup && data.backup.message) console.error('Backup message: ' + data.backup.message); // backup error message
fetchBackups();
$scope.createBackup.busy = false;
return;
+53 -8
View File
@@ -1,8 +1,53 @@
<center>
<h1>Welcome to your Cloudron</h1>
<br/>
<h3 class="">This is your <b>{{ wizard.hostname }}</b> and all your apps will be installed under this domain.</h3>
<br/>
<br/>
<a class="btn btn-primary" href="#/step2" autofocus>Forward</a>
</center>
<div class="row">
<div class="col-md-12 text-center">
<h1>Welcome to your Cloudron!</h1>
<hr/>
<h3 class="">
Choose a name and avatar for your Cloudron
</h3>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<img id="previewAvatar" width="98" height="98" ng-src="{{wizard.avatar.data || wizard.avatar.url}}"/>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<div class="form-group" ng-class="{ 'has-error': setup_form.name.$dirty && setup_form.name.$invalid }">
<!-- <label class="control-label" for="inputName">Name</label> -->
<input type="text" class="form-control" ng-model="wizard.name" id="inputName" name="name" placeholder="Name" ng-enter="next('/step2', setup_form.name.$invalid)" ng-maxlength="512" ng-minlength="1" autofocus required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 settings-avatar-selector">
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
<br/>
<div class="grid">
<div class="item" ng-repeat="avatar in wizard.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="wizard.setPreviewAvatar(avatar)"></div>
<div class="item add" ng-click="showCustomAvatarSelector()"></div>
</div>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step2" ng-disabled="setup_form.name.$invalid">Next</a>
</div>
</div>
+10 -26
View File
@@ -1,43 +1,27 @@
<div class="row">
<div class="col-md-12 text-center">
<h1>Personalize your Cloudron</h1>
<h1>Create an Administrator for <b>{{ wizard.name }}</b></h1>
<h4 class="">
Make it truly yours, by giving your Cloudron an avatar and name.
This admin account is separate from your <a href="https://cloudron.io">cloudron.io</a> account.
</h4>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 settings-avatar-selector">
<img id="previewAvatar" width="128" height="128" ng-src="{{wizard.avatar.data || wizard.avatar.url}}"/>
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
<br/>
<br/>
<div class="grid">
<div class="item" ng-repeat="avatar in wizard.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="wizard.setPreviewAvatar(avatar)"></div>
<div class="item add" ng-click="showCustomAvatarSelector()"></div>
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<div class="form-group" ng-class="{ 'has-error': setup_form.name.$dirty && setup_form.name.$invalid }">
<!-- <label class="control-label" for="inputName">Name</label> -->
<input type="text" class="form-control" ng-model="wizard.name" id="inputName" name="name" placeholder="Name" ng-enter="next('/step3', setup_form.name.$invalid)" ng-maxlength="512" ng-minlength="1" autofocus required autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': setup_form.username.$dirty && setup_form.username.$invalid }">
<!-- <label class="control-label" for="inputUsername">Username</label> -->
<input type="text" class="form-control" ng-model="wizard.username" id="inputUsername" name="username" placeholder="Username" ng-enter="focusNext('inputPassword', setup_form.username.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next('/step3', setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step3" ng-disabled="setup_form.name.$invalid">Next</a>
<a class="btn btn-primary" href="#/step3" ng-disabled="setup_form.username.$invalid">Done</a>
</div>
</div>
+8 -26
View File
@@ -1,26 +1,8 @@
<div class="row">
<div class="col-md-12 text-center">
<h1>Create an Administrator for <b>{{ wizard.name }}</b>, your Cloudron</h1>
<h4 class="">
You can create more users once we are done here.
</h4>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<div class="form-group" ng-class="{ 'has-error': setup_form.username.$dirty && setup_form.username.$invalid }">
<!-- <label class="control-label" for="inputUsername">Username</label> -->
<input type="text" class="form-control" ng-model="wizard.username" id="inputUsername" name="username" placeholder="Username" ng-enter="focusNext('inputPassword', setup_form.username.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next('/step4', setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step4" ng-disabled="setup_form.username.$invalid">Done</a>
</div>
</div>
<center>
<h1>All done!</h1>
<br/>
<br/>
<i class="fa fa-spinner fa-pulse fa-5x"></i>
<br/>
<br/>
</center>
-8
View File
@@ -1,8 +0,0 @@
<center>
<h1>All done!</h1>
<br/>
<br/>
<i class="fa fa-spinner fa-pulse fa-5x"></i>
<br/>
<br/>
</center>
+56
View File
@@ -0,0 +1,56 @@
<div class="content support">
<div>
<div class="text-left">
<h1>Support</h1>
</div>
</div>
<div class="card card-large">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
If you have any questions, please revise our <a href="{{ config.webServerOrigin }}/faq.html" target="_blank">Frequently Asked Questions</a>. We add more answers as we go, if you couldn't find your answers, just let us know using the forms below.<br/>
<br/>
For any developer related issues, please see our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">Developer documentation</a>.
</div>
</div>
</div>
</div>
<br/>
<div class="card card-large">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Feedback</h3>
We would love to hear any ideas or feature requests from your side.<br/>
If you found any issue or bugs, let us know as well, we will resolve that for you as soon as possible.
<br/>
<br/>
<form name="feedbackForm" ng-submit="submitFeedback()">
<div class="form-group">
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required>
<option value="feedback">Enhancement / Idea</option>
<option value="ticket">Bug Report</option>
<option value="app">Missing App</option>
</select>
</div>
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
<input type="text" class="form-control" name="subject" placeholder="Enter your idea or issue" ng-model="feedback.subject" ng-maxlength="512" ng-minlength="1" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.description.$dirty && feedbackForm.description.$invalid) }">
<textarea class="form-control" name="description" rows="3" placeholder="Describe your idea or issue" ng-model="feedback.description" ng-minlength="1" required></textarea>
</div>
<button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-spinner fa-pulse" ng-show="feedback.busy"></i> Submit</button>
<span ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</span>
<span ng-show="feedback.success" class="text-success text-bold">Thank You!</span>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>
+40
View File
@@ -0,0 +1,40 @@
'use strict';
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
$scope.config = Client.getConfig();
$scope.feedback = {
error: null,
success: false,
busy: false,
subject: '',
type: '',
description: ''
};
function resetFeedback() {
$scope.feedback.subject = '';
$scope.feedback.description = '';
$scope.feedback.type = '';
$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;
resetFeedback();
}
$scope.feedback.busy = false;
});
};
}]);
+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">
<!-- <img src="img/box.png" style="transform: scale({{ size.price/50.0 }});"/><br/> -->
<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-show="size.slug === currentSize.slug" data-toggle="tooltip" data-placement="top" title="Your Current Model"><i class="fa fa-check"></i></button>
</div>