Compare commits

..

20 Commits

Author SHA1 Message Date
Johannes Zellner 30b248a0f6 Allow non published versions to be shown if explicitly requested
Fixes #468
2015-08-10 16:16:40 +02:00
Johannes Zellner 7168455de3 Do not use table layout for login view
Fixes #458
2015-08-10 15:26:45 +02:00
Johannes Zellner 085f63e3c7 Show cloudron name in login screen 2015-08-10 15:04:12 +02:00
Johannes Zellner 015be64923 Show cloudron avatar in login screen 2015-08-10 15:01:58 +02:00
Johannes Zellner 2c2471811d Restructure the login page 2015-08-10 14:51:04 +02:00
Johannes Zellner 1025249e93 Since addons are optional, ensure we have a valid empty object in the db 2015-08-10 10:37:55 +02:00
Johannes Zellner 41ffc4bcf3 If we have an empty app search show modal dialog link 2015-08-09 15:19:21 +02:00
Johannes Zellner 2739d54cc1 Make appstore feedback form a modal dialog 2015-08-09 14:48:00 +02:00
Girish Ramakrishnan c4c463cbc2 collect logs using a sudo script
docker logs can only be read by root
2015-08-08 19:04:59 -07:00
Girish Ramakrishnan 8cd13bd43f Update safetydance 2015-08-08 18:53:16 -07:00
Girish Ramakrishnan e4ef279759 Update safetydance and lastmile 2015-08-06 13:54:15 -07:00
Girish Ramakrishnan cf7fecb57b bump cloudron-manifestformat 2015-08-06 13:50:27 -07:00
Girish Ramakrishnan 226041dcb1 Display settings path
Fixes #465
2015-08-06 13:44:09 -07:00
Johannes Zellner 7548025561 If an app search is empty, show hint to give feedback 2015-08-06 18:35:08 +02:00
Johannes Zellner fdbee427ee Show app feedback form in appstore
Fixes #461
2015-08-06 18:30:49 +02:00
Johannes Zellner d861d6d6e4 Properly offset the footer in support view 2015-08-06 18:30:25 +02:00
Johannes Zellner 0a648edcaa Add app feedback category 2015-08-06 17:34:40 +02:00
Johannes Zellner 18850c1fba Cloudron prices are in cents 2015-08-06 16:24:19 +02:00
Girish Ramakrishnan f6df4cab67 Remove ADMIN_ORIGIN 2015-08-05 17:27:55 -07:00
Johannes Zellner 019d29c5b7 Use assert.strictEqual() to see the values 2015-08-05 17:49:19 +02:00
23 changed files with 362 additions and 138 deletions
+8 -38
View File
@@ -7,53 +7,23 @@
// !! No console.log() allowed
// !! Do not set DEBUG
var supervisor = require('supervisord-eventlistener'),
var assert = require('assert'),
mailer = require('./src/mailer.js'),
safe = require('safetydance'),
assert = require('assert'),
exec = require('child_process').exec,
util = require('util'),
mailer = require('./src/mailer.js');
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);
var boxLogData = safe.fs.readFileSync(logFilePath, 'utf-8');
if (boxLogData === null) return callback(safe.error);
var boxLogLines = boxLogData.split('\n').slice(-100);
var dockerLogPath = '/var/log/upstart/docker.log';
var dockerLogData = safe.fs.readFileSync(dockerLogPath, 'utf-8');
if (dockerLogData === null) return callback(safe.error);
var dockerLogLines = dockerLogData.split('\n').slice(-100);
exec('dmesg', function (error, stdout /*, stderr */) {
if (error) console.error(error);
var lines = stdout.split('\n');
var dmesgLogLines = lines.slice(-100);
var result = '';
result += program + '.log\n';
result += '-------------------------------------\n';
result += boxLogLines.join('\n');
result += '\n\n';
result += 'dmesg\n';
result += '-------------------------------------\n';
result += dmesgLogLines.join('\n');
result += '\n\n';
result += 'docker\n';
result += '-------------------------------------\n';
result += dockerLogLines.join('\n');
callback(null, result);
});
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
}
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) {
+37 -38
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,81 +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",
"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"
}
}
@@ -2261,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",
+3 -3
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",
+3
View File
@@ -24,3 +24,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
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
-1
View File
@@ -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());
+2 -1
View File
@@ -19,6 +19,7 @@ exports = module.exports = {
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
sendFeedback: sendFeedback
};
@@ -288,7 +289,7 @@ function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK);
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
var mailOptions = {
from: config.get('adminEmail'),
+1 -1
View File
@@ -25,4 +25,4 @@
</head>
<body>
<body class="oauth">
+32 -22
View File
@@ -1,32 +1,40 @@
<% include header %>
<center>
<h1>Login to <%= applicationName %></h1>
</center>
<% if (error) { %>
<center>
<br/><br/>
<h4 class="has-error"><%= error %></h4>
</center>
<% } %>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
<div class="card">
<div class="row">
<div class="col-md-12">
<h1><img width="64" height="64" src="<%= applicationLogo %>"/> Login to <%= applicationName %> on <%= cloudronName %></h1>
</div>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" required>
<br/>
<% if (error) { %>
<div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
<% } %>
<div class="row">
<div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -34,6 +42,8 @@
<script>
(function () {
'use strict';
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo;
+6
View File
@@ -119,6 +119,9 @@ function installApp(req, res, next) {
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
// addons is optional
data.manifest.addons = data.manifest.addons || {};
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);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, function (error) {
@@ -253,6 +256,9 @@ function updateApp(req, res, next) {
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
// addons is optional
data.manifest.addons = data.manifest.addons || {};
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings, data.icon, function (error) {
+1 -1
View File
@@ -163,7 +163,7 @@ function setCertificate(req, res, next) {
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET) return next(new HttpError(400, 'type must be either "ticket" or "feedback"'));
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
+31 -20
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
querystring = require('querystring'),
util = require('util'),
session = require('connect-ensure-login'),
settings = require('../settings.js'),
tokendb = require('../tokendb'),
appdb = require('../appdb'),
url = require('url'),
@@ -188,37 +189,47 @@ function loginForm(req, res) {
var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
function render(applicationName) {
var cloudronName = '';
function render(applicationName, applicationLogo) {
res.render('login', {
adminOrigin: config.adminOrigin(),
csrf: req.csrfToken(),
cloudronName: cloudronName,
applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null
});
}
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
settings.getCloudronName(function (error, name) {
if (error) return sendError(req, res, 'Internal Error');
// Handle our different types of oauth clients
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME);
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME);
} else if (appId.indexOf('external-') === 0) {
return render('External Application');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
cloudronName = name;
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
var applicationName = result.location || config.fqdn();
render(applicationName);
// Handle our different types of oauth clients
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/cloudron/avatar');
});
});
});
}
+2 -1
View File
@@ -573,7 +573,8 @@ describe('App installation', function () {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
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();
+11
View File
@@ -588,6 +588,17 @@ describe('Cloudron', function () {
});
});
it('succeeds with app type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without description', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject' })
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -eu -o pipefail
if [[ $EUID -ne 0 ]]; then
echo "This script should be run as root." >&2
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [ $# -lt 1 ]; then
echo "Usage: collectlogs.sh <program>"
exit 1
fi
readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
tail --lines=100 /var/log/supervisor/${program_name}.log
echo
echo
echo "dmesg"
echo "-----"
dmesg | tail --lines=100
echo
echo
echo "docker"
echo "------"
tail --lines=100 /var/log/upstart/docker.log
echo
echo
+1 -1
View File
@@ -54,7 +54,7 @@ function uninitialize(callback) {
function startNextTask() {
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());
}
+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
+11
View File
@@ -63,6 +63,17 @@ angular.module('Application').service('AppStore', ['$http', 'Client', function (
});
};
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
+52
View File
@@ -206,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;
@@ -640,6 +665,33 @@ footer a {
}
// ----------------------------
// Oauth classes
// ----------------------------
.oauth {
height: 100%;
width: 100%;
padding: 0;
background: #F7F7F7;
h1 {
margin-top: 0;
}
.card {
max-width: none;
padding: 20px;
text-align: left;
margin-top: 15px;
@media(min-width:768px) {
margin-top: 20%;
}
}
}
// ----------------------------
// Graphs classes
// ----------------------------
+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) }">
+47 -1
View File
@@ -64,6 +64,47 @@
</div>
</div>
<!-- Modal feedback -->
<div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">App Feedback</h4>
</div>
<div class="modal-body">
<fieldset>
<form name="feedbackForm" ng-submit="submitFeedback()">
<div ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</div>
<textarea class="form-control" id="feedbackDescriptionTextarea" cols="3" ng-model="feedback.description" ng-minlength="1" required placeholder="Name, Category, Links ..." autofocus></textarea>
<input class="ng-hide" type="submit" ng-disabled="feedbackForm.$invalid || feedback.busy"/>
</form>
</fieldset>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="submitFeedback()" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-fw fa-paper-plane"></i> Submit</button>
</div>
</div>
</div>
</div>
<!-- Modal app not found -->
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">App not found</h4>
</div>
<div class="modal-body">
There is no such app <b>{{ appNotFound.appId }}</b><span ng-show="appNotFound.version"> with version <b>{{ appNotFound.version }}</b></span>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
<div>
<div class="row-no-margin">
<div class="col-md-2">
@@ -98,6 +139,10 @@
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki">Wiki</a>
<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/>
<a href="" ng-click="showFeedbackModal()">Missing an app? Let us know.</a>
</div>
<div class="col-md-10" ng-show="ready && apps.length">
<div class="row-no-margin">
@@ -117,7 +162,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="showFeedbackModal()"><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,47 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
mediaLinks: []
};
$scope.appNotFound = {
appId: '',
version: ''
};
$scope.feedback = {
error: null,
success: false,
subject: 'App feedback',
description: '',
type: 'app'
};
function resetFeedback() {
$scope.feedback.description = '';
$scope.feedbackForm.$setUntouched();
$scope.feedbackForm.$setPristine();
}
$scope.submitFeedback = function () {
$scope.feedback.busy = true;
$scope.feedback.success = false;
$scope.feedback.error = null;
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
if (error) {
$scope.feedback.error = error;
} else {
$scope.feedback.success = true;
$('#feedbackModal').modal('hide');
resetFeedback();
}
$scope.feedback.busy = false;
});
};
$scope.showFeedbackModal = function () {
$('#feedbackModal').modal('show');
};
function getAppList(callback) {
AppStore.getApps(function (error, apps) {
@@ -132,6 +173,13 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
}
};
$scope.showAppNotFound = function (appId, version) {
$scope.appNotFound.appId = appId;
$scope.appNotFound.version = version;
$('#appNotFoundModal').modal('show');
};
$scope.doInstall = function () {
$scope.appInstall.busy = true;
$scope.appInstall.error.other = null;
@@ -189,11 +237,26 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// show install app dialog immediately if an app id was passed in the query
if ($routeParams.appId) {
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]);
if ($routeParams.version) {
AppStore.getAppByIdAndVersion($routeParams.appId, $routeParams.version, function (error, result) {
if (error) {
$scope.showAppNotFound($routeParams.appId, $routeParams.version);
console.error(error);
return;
}
$scope.showInstall(result);
});
} else {
var found = apps.filter(function (app) {
return (app.id === $routeParams.appId) && ($routeParams.version ? $routeParams.version === app.manifest.version : true);
});
if (found.length) {
$scope.showInstall(found[0]);
} else {
$scope.showAppNotFound($routeParams.appId, null);
}
}
}
@@ -204,7 +267,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
refresh();
// setup all the dialog focus handling
['appInstallModal'].forEach(function (id) {
['appInstallModal', 'feedbackModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
+3 -3
View File
@@ -33,6 +33,7 @@
<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) }">
@@ -51,6 +52,5 @@
</div>
</div>
<br/>
<br/>
<br/>
<!-- Offset the footer -->
<br/><br/>
+1 -1
View File
@@ -77,7 +77,7 @@
<div class="cloudron-model-item-content shadow" ng-class="{ 'selected': size.slug === currentSize.slug }" style="height: {{ 120 + $index * 30 }}px">
<!-- <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>