Compare commits

..

16 Commits

Author SHA1 Message Date
Girish Ramakrishnan fa6d151325 Fix update mail templates 2017-11-02 21:34:03 -07:00
Girish Ramakrishnan a7296a0339 Rename filename to backupId in backup eventlog 2017-11-02 18:17:08 -07:00
Girish Ramakrishnan a6aee53ec2 Filter out failed backups 2017-11-02 18:13:51 -07:00
Girish Ramakrishnan 963ab2e791 More 1.7.7 changes 2017-11-02 16:30:13 -07:00
Girish Ramakrishnan ca724b8b03 Add cert renewal and user add/remove in weekly digest 2017-11-02 16:30:10 -07:00
Girish Ramakrishnan 88a929c85e Instead of appstore account, include owner alternate email 2017-11-02 15:10:05 -07:00
Girish Ramakrishnan 2bc0270880 1.7.7 changes 2017-11-02 12:18:51 -07:00
Girish Ramakrishnan 014b77b7aa Fix LE cert renewal failures
LE contacts the server by hostname and not by IP. This means that
when installing and reconfiguring the app it hits the default_server
route since nginx configs for the app are not generated at.

When doing in the daily cert renew, the nginx configs exist and we
are unable to renew the certs.
2017-11-02 11:43:43 -07:00
Girish Ramakrishnan 06f8aa8f29 Remove dead code
getNonApprovedCode code flow is ununsed (and broken by design on
the appstore side).
2017-11-02 10:36:30 -07:00
Girish Ramakrishnan a8c64bf9f7 Clarify heartbeat code
heartbeats are not sent for self-hosted cloudrons (only managed ones)
2017-11-02 10:26:21 -07:00
Girish Ramakrishnan 41ef16fbec link to memory limit docs 2017-11-01 09:25:05 -07:00
Girish Ramakrishnan 2a848a481b Add newline 2017-11-01 09:25:05 -07:00
Johannes Zellner 3963d76a80 The update dialog does not contain a form anymore
Fixes #467
2017-11-01 11:55:06 +01:00
Girish Ramakrishnan 8ede37a43d Make the dkim selector dynamic
it has to change with the adminLocation so that multiple cloudrons
can send out emails at the same time.
2017-10-31 12:18:40 -07:00
Girish Ramakrishnan 36534f6bb2 Fix indent 2017-10-31 12:12:02 -07:00
Girish Ramakrishnan 7eddcaf708 Allow setting app memory till memory limit
Fixes #466
2017-10-31 12:12:02 -07:00
28 changed files with 311 additions and 238 deletions
+8
View File
@@ -1094,3 +1094,11 @@
* New mail container release that fixes email sending with SOGo
* Show 404 page for unknown domains
[1.7.7]
* Allow setting app memory till memory limit
* Make the dkim selector dynamic
* Fix issue where app update dialog did not close
* Fix LE cert renewal failures
* Send user and cert info in digest emails
* Send oom, app failures and other important mails to cloudron owner's alt mail
+6
View File
@@ -26,6 +26,12 @@ server {
}
<% } -%>
# acme challenges (for cert renewal where the vhost config exists)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
+1 -1
View File
@@ -43,7 +43,7 @@ http {
listen [::]:80 default_server;
server_name does_not_match_anything;
# acme challenges
# acme challenges (for app installation and re-configure when the vhost config does not exist)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
+3 -3
View File
@@ -792,12 +792,12 @@ function backupBoxAndApps(auditSource, callback) {
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, filename) {
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
callback(error, filename);
callback(error, backupId);
});
});
});
+4 -4
View File
@@ -12,7 +12,7 @@ exports = module.exports = {
dnsSetup: dnsSetup,
getLogs: getLogs,
sendHeartbeat: sendHeartbeat,
sendCaasHeartbeat: sendCaasHeartbeat,
updateToLatest: updateToLatest,
reboot: reboot,
@@ -440,8 +440,8 @@ function getConfig(callback) {
});
}
function sendHeartbeat() {
if (config.provider() !== 'caas') return;
function sendCaasHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
@@ -543,7 +543,7 @@ function addDnsRecords(ip, callback) {
var webadminRecord = { subdomain: config.adminLocation(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
if (config.isCustomDomain()) {
+8 -1
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
token: token,
version: version,
@@ -33,9 +34,9 @@ exports = module.exports = {
mailLocation: mailLocation,
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isDemo: isDemo,
@@ -248,3 +249,9 @@ function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
}
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
-2
View File
@@ -33,8 +33,6 @@ exports = module.exports = {
DEMO_USERNAME: 'cloudron',
DKIM_SELECTOR: 'cloudron',
AUTOUPDATE_PATTERN_NEVER: 'never'
};
+17 -15
View File
@@ -35,7 +35,7 @@ var gAliveJob = null, // send periodic stats
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gDynamicDNSJob = null,
gHeartbeatJob = null, // for CaaS health check
gCaasHeartbeatJob = null, // for CaaS health check
gSchedulerSyncJob = null,
gDigestEmailJob = null;
@@ -53,18 +53,20 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: false
});
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
setTimeout(function () {
if (!gHeartbeatJob) return; // already uninitalized
gHeartbeatJob.start();
cloudron.sendHeartbeat();
}, 1000 * 60);
if (config.provider() === 'caas') {
gCaasHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendCaasHeartbeat,
start: false
});
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
setTimeout(function () {
if (!gCaasHeartbeatJob) return; // already uninitalized
gCaasHeartbeatJob.start();
cloudron.sendCaasHeartbeat();
}, 1000 * 60);
}
var randomHourMinute = Math.floor(60*Math.random());
gAliveJob = new CronJob({
@@ -252,8 +254,8 @@ function uninitialize(callback) {
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = null;
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = null;
if (gCaasHeartbeatJob) gCaasHeartbeatJob.stop();
gCaasHeartbeatJob = null;
if (gAliveJob) gAliveJob.stop();
gAliveJob = null;
+1 -21
View File
@@ -7,19 +7,15 @@ exports = module.exports = {
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
issueDeveloperToken: issueDeveloperToken
};
var assert = require('assert'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -84,19 +80,3 @@ function issueDeveloperToken(user, auditSource, callback) {
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
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() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401 || result.statusCode === 403) {
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
return callback(null, []);
}
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);
});
}
+25 -18
View File
@@ -33,31 +33,38 @@ function maybeSend(callback) {
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
if (error) return callback(error);
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
var info = {
hasSubscription: hasSubscription,
if (error) return callback(error);
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
var info = {
hasSubscription: hasSubscription,
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
};
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
} else {
debug('maybeSend: nothing happened, NOT sending digest email');
}
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
callback();
});
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
callback();
});
});
});
+1 -1
View File
@@ -132,7 +132,7 @@ function verifyRelay(relay, callback) {
function checkDkim(callback) {
var dkim = {
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
+8 -8
View File
@@ -6,7 +6,7 @@ exports = module.exports = {
add: add,
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
@@ -98,21 +98,21 @@ function getAllPaged(action, search, page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
callback(null, events);
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
eventlogdb.getByCreationTime(creationTime, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
callback(null, events);
});
}
@@ -120,7 +120,7 @@ function cleanup(callback) {
callback = callback || NOOP_CALLBACK;
var d = new Date();
d.setDate(d.getDate() - 7); // 7 days ago
d.setDate(d.getDate() - 10); // 10 days ago
// only cleanup high frequency events
var actions = [
+5 -5
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
getByCreationTime: getByCreationTime,
add: add,
count: count,
delByCreationTime: delByCreationTime,
@@ -73,12 +73,12 @@ function getAllPaged(action, search, page, perPage, callback) {
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE action=? AND creationTime >= DATE_SUB(NOW(), INTERVAL 1 WEEK) ORDER BY creationTime DESC';
database.query(query, [ action ], function (error, results) {
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ?';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
+2 -2
View File
@@ -7,7 +7,7 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.7.0',
'version': '48.8.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
@@ -18,7 +18,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.38.1' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
}
};
+35 -3
View File
@@ -2,13 +2,14 @@
Dear Cloudron Admin,
a new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
Changes:
<%= updateInfo.manifest.changelog %>
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
@@ -16,4 +17,35 @@ Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<div style="width: 650px; text-align: left;">
<p>
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
</p>
<h5>Changelog:</h5>
<%- changelogHTML %>
<br/>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } %>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
<% } %>
+12 -9
View File
@@ -4,15 +4,18 @@ Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
* <%- changelog[i] %>
<% } %>
Thank you,
your Cloudron
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
@@ -27,11 +30,6 @@ your Cloudron
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
</p>
<p>
Your Cloudron will update automatically tonight.<br/>
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
</p>
<h5>Changelog:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
@@ -40,6 +38,11 @@ your Cloudron
</ul>
<br/>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } %>
<br/>
</div>
+47 -5
View File
@@ -2,7 +2,19 @@
Dear <%= cloudronName %> Admin,
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
This is a summary of the activities on your Cloudron <%= fqdn %>.
<% if (info.usersAdded.length) { -%>
The following users were added:
<% for (var i = 0; i < info.usersAdded.length; i++) { -%>
* <%- info.usersAdded[i].email %>
<% }} -%>
<% if (info.certRenewals.length) { -%>
The certificates of the following apps was renewed:
<% for (var i = 0; i < info.certRenewals.length; i++) { -%>
* <%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %>
<% }} -%>
<% if (info.pendingBoxUpdate) { -%>
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
@@ -33,6 +45,14 @@ The following apps were updated:
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
<% }}} -%>
<% if (info.finishedBackups.length) { -%>
Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBackups[0].filename %>
<% } else { -%>
This Cloudron did **not** backup successfully in the last week!
<% } -%>
<% if (!info.hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
@@ -52,9 +72,25 @@ Sent at: <%= new Date().toUTCString() %>
<br/>
<p>Weekly summary of activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a>:</p>
<p>This is a summary of the activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a> last week.</p>
<br/>
<% if (info.usersAdded.length) { -%>
<p><b>The following users were added:</b></p>
<ul>
<% for (var i = 0; i < info.usersAdded.length; i++) { %>
<li><%- info.usersAdded[i].email %></li>
<% } %>
</ul>
<% } %>
<% if (info.certRenewals.length) { -%>
<p><b>The certificates of the following apps were renewed:</b></p>
<ul>
<% for (var i = 0; i < info.certRenewals.length; i++) { %>
<li><%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %></li>
<% } %>
</ul>
<% } %>
<% if (info.pendingBoxUpdate) { -%>
<p><b>Cloudron v<%- info.pendingBoxUpdate.version %> is available:</b></p>
@@ -113,6 +149,12 @@ Sent at: <%= new Date().toUTCString() %>
</ul>
<% } %>
<% if (info.finishedBackups.length) { %>
<p><b>Last successful backup : </b> <%= info.finishedBackups[0].backupId || info.finishedBackups[0].filename %> </p>
<% } else { %>
<p><b>This Cloudron did not backup successfully in the last week!</b></p>
<% } %>
<br/>
<% if (!info.hasSubscription) { %>
@@ -123,12 +165,12 @@ Sent at: <%= new Date().toUTCString() %>
<br/>
<br/>
<p style="text-align: right;">
<center>
<small>
Powered by <a href="https://cloudron.io">Cloudron</a><br/>
Sent on <%= new Date().toUTCString() %>
</small>
</p>
</center>
</div>
</center>
+76 -50
View File
@@ -171,6 +171,7 @@ function getAdminEmails(callback) {
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
@@ -244,7 +245,7 @@ function userAdded(user, inviteSent) {
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
@@ -341,7 +342,7 @@ function appDied(app) {
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -354,12 +355,13 @@ function appDied(app) {
});
}
function boxUpdateAvailable(newBoxVersion, changelog) {
function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof newBoxVersion, 'string');
assert(util.isArray(changelog));
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
@@ -373,6 +375,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: cloudronName,
@@ -398,29 +401,13 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
});
}
function appUpdateAvailable(app, updateInfo) {
function appUpdateAvailable(app, hasSubscription, info) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Update available for %s', config.fqdn(), app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
};
enqueue(mailOptions);
});
}
function sendDigest(info) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
@@ -428,34 +415,73 @@ function sendDigest(info) {
cloudronName = 'Cloudron';
}
appstore.getAccount(function (error, appstoreProfile) {
if (error && error.reason !== AppstoreError.BILLING_REQUIRED) console.error(error);
if (appstoreProfile) adminEmails.push(appstoreProfile.email);
var converter = new showdown.Converter();
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
enqueue(mailOptions);
});
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
@@ -464,7 +490,7 @@ function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -481,7 +507,7 @@ function backupFailed(error) {
var message = splatchError(error);
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -499,7 +525,7 @@ function certificateRenewalError(domain, message) {
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -517,7 +543,7 @@ function oomEvent(program, context) {
assert.strictEqual(typeof context, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
+1 -1
View File
@@ -254,7 +254,7 @@ function createMailConfig(callback) {
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
+1 -8
View File
@@ -4,8 +4,7 @@ exports = module.exports = {
enabled: enabled,
setEnabled: setEnabled,
status: status,
login: login,
apps: apps
login: login
};
var developer = require('../developer.js'),
@@ -52,9 +51,3 @@ 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 }));
});
}
+25 -26
View File
@@ -59,26 +59,26 @@ function initializeExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
.use(middleware.session({
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
resave: true,
saveUninitialized: true,
cookie: {
path: '/',
httpOnly: true,
secure: process.env.BOX_ENV !== 'test',
maxAge: 600000
}
}))
.use(passport.initialize())
.use(passport.session())
.use(router)
.use(middleware.lastMile());
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
.use(middleware.session({
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
resave: true,
saveUninitialized: true,
cookie: {
path: '/',
httpOnly: true,
secure: process.env.BOX_ENV !== 'test',
maxAge: 600000
}
}))
.use(passport.initialize())
.use(passport.session())
.use(router)
.use(middleware.lastMile());
// NOTE: these limits have to be in sync with nginx limits
var FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
@@ -108,7 +108,6 @@ 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);
// cloudron routes
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
@@ -284,11 +283,11 @@ function initializeSysadminExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(router)
.use(middleware.lastMile());
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(router)
.use(middleware.lastMile());
// Sysadmin routes
router.post('/api/v1/backup', routes.sysadmin.backup);
+4 -10
View File
@@ -16,6 +16,7 @@ var async = require('async'),
paths = require('../paths.js'),
safe = require('safetydance'),
settings = require('../settings.js'),
settingsdb = require('../settingsdb.js'),
updatechecker = require('../updatechecker.js'),
user = require('../user.js');
@@ -113,7 +114,7 @@ describe('digest', function () {
});
});
it('sends mail for pending update to appstore account email (caas)', function (done) {
it('sends mail for pending update to owner account email', function (done) {
var subscription = {
id: 'caas',
created: 0,
@@ -123,23 +124,16 @@ describe('digest', function () {
};
updatechecker._setUpdateInfo({ box: null, apps: { 'appid': { manifest: { version: '1.2.5', changelog: 'noop\nreally' } } } });
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/test-user/cloudrons') >= 0; }).reply(201, { cloudron: { id: 'test-cloudron' }});
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/test-user/cloudrons/test-cloudron/subscription') >= 0; }).reply(200, { subscription: subscription });
var fake3 = nock(config.apiServerOrigin()).get('/api/v1/users/test-user?accessToken=test-token').reply(200, { profile: { id: 'test-user', email: 'test@email.com' } });
settings.setAppstoreConfig({ userId: 'test-user', token: 'test-token', cloudronId: 'test-cloudron' }, function (error) {
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) {
if (error) return done(error);
digest.maybeSend(function (error) {
if (error) return done(error);
checkMails(1, 'test@email.com', function (error) {
checkMails(1, [ 'user0@email.com, username0@localhost' ], function (error) {
if (error) return done(error);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
expect(fake3.isDone()).to.be.ok();
done();
});
});
+4 -4
View File
@@ -113,7 +113,7 @@ function checkAppUpdates(callback) {
// always send notifications if user is on the free plan
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
mailer.appUpdateAvailable(app, updateInfo);
mailer.appUpdateAvailable(app, false /* subscription */, updateInfo);
return iteratorDone();
}
@@ -123,7 +123,7 @@ function checkAppUpdates(callback) {
debug(error);
} else if (result === constants.AUTOUPDATE_PATTERN_NEVER) {
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
mailer.appUpdateAvailable(app, updateInfo);
mailer.appUpdateAvailable(app, true /* hasSubscription */, updateInfo);
}
iteratorDone();
@@ -169,14 +169,14 @@ function checkBoxUpdates(callback) {
// always send notifications if user is on the free plan
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
mailer.boxUpdateAvailable(false /* hasSubscription */, updateInfo.version, updateInfo.changelog);
return done();
}
// only send notifications if update pattern is 'never'
settings.getAutoupdatePattern(function (error, result) {
if (error) debug(error);
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(true /* hasSubscription */, updateInfo.version, updateInfo.changelog);
done();
});
+8 -7
View File
@@ -71,14 +71,14 @@ function getByEmail(email, callback) {
function getOwner(callback) {
assert.strictEqual(typeof callback, 'function');
// the first created user it the admin
// the first created user it the 'owner'
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY createdAt LIMIT 1',
[ constants.ADMIN_GROUP_ID ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
[ constants.ADMIN_GROUP_ID ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(result[0]));
});
callback(null, postProcess(result[0]));
});
}
function getByResetToken(resetToken, callback) {
@@ -116,7 +116,8 @@ function getAllWithGroupIds(callback) {
function getAllAdmins(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY username', [ constants.ADMIN_GROUP_ID ], function (error, results) {
// the mailer code relies on the first object being the 'owner' (thus the ORDER)
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY createdAt', [ constants.ADMIN_GROUP_ID ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
-7
View File
@@ -697,13 +697,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getNonApprovedApps = function (callback) {
get('/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) {
+2 -2
View File
@@ -109,7 +109,7 @@
<a href="" ng-click="appConfigure.advancedVisible = true" ng-hide="appConfigure.advancedVisible">Advanced settings...</a>
<div uib-collapse="!appConfigure.advancedVisible">
<div class="form-group">
<label class="control-label" for="memoryLimit">Maximum Memory Limit: <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
<label class="control-label" for="memoryLimit">Maximum Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#increasing-the-memory-limit-of-an-app" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
<br/>
<div style="padding: 0 10px;">
<slider id="memoryLimit" ng-model="appConfigure.memoryLimit" step="134217728" tooltip="hide" ticks="appConfigure.memoryTicks" ticks-snap-bounds="67108864"></slider>
@@ -302,7 +302,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="doUpdate(appUpdateForm)" ng-disabled="appUpdateForm.$invalid || appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
</div>
</div>
</div>
+7 -13
View File
@@ -252,14 +252,12 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.robotsTxt = app.robotsTxt;
$scope.appConfigure.enableBackup = app.enableBackup;
// create ticks starting from manifest memory limit
$scope.appConfigure.memoryTicks = [
256 * 1024 * 1024,
512 * 1024 * 1024,
1024 * 1024 * 1024,
2048 * 1024 * 1024,
4096 * 1024 * 1024
].filter(function (t) { return t >= (app.manifest.memoryLimit || 0); });
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
$scope.appConfigure.memoryTicks = [ ];
for (var i = 256; i <= ($scope.config.memory*2/1024/1024); i *= 2) {
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024);
}
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
}
@@ -407,7 +405,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$('#appUpdateModal').modal('show');
};
$scope.doUpdate = function (form) {
$scope.doUpdate = function () {
$scope.appUpdate.busy = true;
Client.updateApp($scope.appUpdate.app.id, $scope.appUpdate.manifest, function (error) {
@@ -415,10 +413,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
Client.error(error);
} else {
$scope.appUpdate.app = {};
form.$setPristine();
form.$setUntouched();
$('#appUpdateModal').modal('hide');
}
-12
View File
@@ -353,18 +353,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
});
return callback(null, apps);
// 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));
// });
});
}