Compare commits

..

53 Commits

Author SHA1 Message Date
Girish Ramakrishnan dcfb7b0de5 Add 1.1.3 changes 2017-08-01 17:02:52 -07:00
Girish Ramakrishnan bdc5d6931c Remove _KEY
This was a bug from 2fb02d57ed
2017-08-01 16:59:45 -07:00
Johannes Zellner 038c4e3c58 Add changes for 1.1.1 and 1.1.2 2017-07-31 19:49:15 +02:00
Johannes Zellner cfd5c3d999 Add 1.0.2 changes 2017-07-31 19:47:56 +02:00
Johannes Zellner 126095969f Fix digest cron schedule to no run every hour on wednesdays 2017-07-31 19:45:33 +02:00
Johannes Zellner 84153793d9 Add 1.0.1 changes 2017-07-25 22:08:34 +02:00
Girish Ramakrishnan 31ad42501b Use -%> for newline slurping
Fixes #383
2017-07-25 22:08:34 +02:00
Johannes Zellner 42476e10e1 Allow digest to be templated with or without subscription 2017-07-25 22:08:30 +02:00
Girish Ramakrishnan 91e5f850ab Adjust digest wording 2017-07-25 22:08:03 +02:00
Johannes Zellner 6ee383185d Add cron job to send email digest 2017-07-25 22:07:50 +02:00
Johannes Zellner 2fb02d57ed Add cron job to send email digest 2017-07-25 21:52:46 +02:00
Johannes Zellner 6b8edbf4f0 send lastLogin event timestamp with alive status 2017-07-25 19:54:08 +02:00
Girish Ramakrishnan 4d7f9ba9a5 isPaying is not set properly for non-caas 2017-06-21 22:38:39 -07:00
Girish Ramakrishnan 6d0cdc36b2 move getSubscription to appstore.js 2017-06-21 22:17:32 -07:00
Girish Ramakrishnan 79541a68a5 Display and send usernames instead of the email address 2017-06-21 19:34:55 -07:00
Girish Ramakrishnan 845d386478 Grammar 2017-06-21 19:28:38 -07:00
Girish Ramakrishnan 8771de5c12 Minor rewording 2017-06-21 19:14:15 -07:00
Girish Ramakrishnan 76246b2952 Try to fix sporadic mysql startup issue after cloudron-setup 2017-06-21 17:20:02 -07:00
Johannes Zellner f994b68701 wait for dns with the correct zone even on external domain setup 2017-06-21 15:04:39 +02:00
Johannes Zellner 77558c823c Check for subscription right after appstore login 2017-06-21 13:43:34 +02:00
Johannes Zellner dd6a19ea85 get zoneName from domain on migration if not set 2017-06-21 13:41:13 +02:00
Johannes Zellner 16978f8c1a Keep the subdomain as is for non-custom domain cloudrons 2017-06-21 10:23:04 +02:00
Johannes Zellner f311c3da1c Fix explicit zone information in dns setup view 2017-06-21 09:51:35 +02:00
Johannes Zellner 423e355fd6 Add changes 2017-06-21 09:37:34 +02:00
Johannes Zellner 8fadb3badc Use the actual result not the potentially cached value 2017-06-20 13:10:07 +02:00
Johannes Zellner 3845065085 Enable catchall based on subscription status 2017-06-20 12:58:14 +02:00
Johannes Zellner 801d848908 Show hint about subdomain cloudrons in dns setup 2017-06-20 11:56:09 +02:00
Girish Ramakrishnan e6eda1283c Format the combo box better 2017-06-19 23:16:03 -07:00
Girish Ramakrishnan a553755f4a the noop callback will print the error 2017-06-19 22:20:25 -07:00
Girish Ramakrishnan cd52459f05 more descriptive debug 2017-06-19 22:20:25 -07:00
Girish Ramakrishnan 1802201e9e Remove one level of indentation 2017-06-19 22:20:22 -07:00
Johannes Zellner 2d72f49261 Ensure the updatechecker does not prematurely callback
Also add tests and make sure we send update notifications if automatic
updates cannot be applied
2017-06-19 14:34:36 +02:00
Johannes Zellner cd42a6c2ea Send update notifications on the free plan 2017-06-19 13:27:08 +02:00
Johannes Zellner 65f949e669 Add settings.getSubscription() 2017-06-19 13:26:49 +02:00
Johannes Zellner f3fec9a33c Handle 402 response on app installation 2017-06-19 12:17:55 +02:00
Johannes Zellner 13182de57f Appstore login dialog does not exist anymore 2017-06-19 12:06:42 +02:00
Girish Ramakrishnan c33566b553 Add note that LE certs require valid email
part of #338
2017-06-18 17:23:41 -07:00
Johannes Zellner 4faf247898 Add catch-all address interface 2017-06-16 21:04:46 +02:00
Johannes Zellner 9952a986eb Always remind the user that the DNS zone has to be hosted on the provider
Do not use $location as the search() object is not consistent without
the angular router, which is not used here
2017-06-16 21:04:44 +02:00
Girish Ramakrishnan 40aaffe365 tests: Fix usage of settings.setDnsConfig 2017-06-15 20:05:35 -07:00
Girish Ramakrishnan 3745e96a6f domain -> fqdn 2017-06-15 19:56:04 -07:00
Girish Ramakrishnan 157ce06f93 Add zoneName query parameter to dns setup
fixes #110
2017-06-15 19:55:48 -07:00
Girish Ramakrishnan 822dfb8af5 Allow 3rd level domains in UI
part of #110
2017-06-15 19:55:32 -07:00
Girish Ramakrishnan 9ead482dc6 Make verifyDnsConfig take zone name
part of #110
2017-06-15 19:55:24 -07:00
Girish Ramakrishnan 865c0a7aa7 Pass other level domains to dns API backends
part of #110
2017-06-15 19:55:01 -07:00
Girish Ramakrishnan c760c42f92 make waitForDns take zone name argument
part of #110
2017-06-15 19:54:08 -07:00
Girish Ramakrishnan ded31b977e Add config.setFqdn and config.setZoneName
Part of #110
2017-06-15 19:53:20 -07:00
Johannes Zellner 4781c4e364 Deliver empty JSON object on success
This ensures the client does not throw a parsing exception
2017-06-15 07:49:19 -07:00
Johannes Zellner 8e123b017e Add REST wrapper for catchall 2017-06-15 07:49:07 -07:00
Girish Ramakrishnan 658cbcdab9 bump mail container version (catchall support)
part of #33
2017-06-15 07:48:57 -07:00
Girish Ramakrishnan 0cc980f539 Add setting for catch all address
Note that this is not a flag on the mailboxes because we might theoretically
support forwarding to some other external domain in the future.

Part of #33
2017-06-15 07:48:46 -07:00
Girish Ramakrishnan da7648fe3f Match the button text with existing text in the UI 2017-06-14 21:55:17 -07:00
Johannes Zellner 8db1073980 Add changes 2017-06-14 20:29:10 +02:00
50 changed files with 1293 additions and 168 deletions
+22
View File
@@ -885,3 +885,25 @@
* Prevent email view from flickering
* Prepare for 1.0
[1.0.0]
* Make selfhosting great again
[1.0.1]
* Notification improvements
[1.0.2]
* Notification improvements
[1.1.0]
* Add support for email catch-all
* Support Cloudrons on subdomains
[1.1.1]
* Notification improvements
[1.1.2]
* Notification improvements
[1.1.3]
* Notification improvements
+30 -2
View File
@@ -1196,6 +1196,34 @@ Request:
}
```
### Get Catch All Address
GET `/api/v1/settings/catch_all_address` <scope>admin</scope>
Gets the address(es) to which emails addressed to a non-existent mailbox are forwarded to.
Configuring a catch-all address can help avoid losing emails due to misspelling.
Response(200):
```
{
"address": [ <string> ] // array of mailbox names
}
```
### Set Catch All Address
PUT `/api/v1/settings/catch_all_address` <scope>admin</scope>
Sets the address(es) to which emails addressed to a non-existent mailbox are forwarded.
Configuring a catch-all address can help avoid losing emails due to misspelling.
Request:
```
{
"address": [ <string> ] // array of mailbox names
}
```
### Get DNS Configuration
GET `/api/v1/settings/dns_config` <scope>admin</scope> <scope>internal</scope>
@@ -1221,7 +1249,7 @@ This is currently internal API and is documented here for completeness.
### Get Email Configuration
GET `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
GET `/api/v1/settings/mail_config` <scope>admin</scope>
Gets the email configuration. The Cloudron has a built-in email server for users.
This configuration can be used to disable the server. Note that the Cloudron will
@@ -1236,7 +1264,7 @@ Response(200):
### Set Email Configuration
POST `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
POST `/api/v1/settings/mail_config` <scope>admin</scope>
Sets the email configuration. The Cloudron has a built-in email server for users.
This configuration can be used to enable or disable the email server. Note that
+1
View File
@@ -282,5 +282,6 @@ fi
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+66 -36
View File
@@ -4,6 +4,8 @@ exports = module.exports = {
purchase: purchase,
unpurchase: unpurchase,
getSubscription: getSubscription,
sendAliveStatus: sendAliveStatus,
getAppUpdate: getAppUpdate,
@@ -15,6 +17,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
eventlog = require('./eventlog.js'),
os = require('os'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -68,6 +71,25 @@ function getAppstoreConfig(callback) {
}
}
function getSubscription(callback) {
assert.strictEqual(typeof callback, 'function');
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
const url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription';
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'invalid appstore token'));
if (result.statusCode === 403) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'wrong user'));
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'stripe error'));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'unknown error'));
callback(null, result.body.subscription);
});
});
}
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
@@ -82,9 +104,10 @@ function purchase(appId, appstoreId, callback) {
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
@@ -127,45 +150,52 @@ function sendAliveStatus(data, callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY]
};
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
}
};
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
callback(null);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
});
+12 -5
View File
@@ -58,6 +58,7 @@ var appdb = require('./appdb.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
@@ -165,18 +166,24 @@ function onDomainConfigured(callback) {
], callback);
}
function dnsSetup(dnsConfig, domain, callback) {
function dnsSetup(dnsConfig, domain, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
settings.setDnsConfig(dnsConfig, domain, function (error) {
if (!zoneName) zoneName = tld.getDomain(domain) || '';
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
settings.setDnsConfig(dnsConfig, domain, zoneName, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.set('fqdn', domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setZoneName(zoneName);
async.series([ // do not block
onDomainConfigured,
@@ -855,9 +862,9 @@ function migrate(options, callback) {
if (!options.domain) return doMigrate(options, callback);
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token');
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token', 'zoneName');
settings.setDnsConfig(dnsConfig, options.domain, function (error) {
settings.setDnsConfig(dnsConfig, options.domain, options.zoneName || tld.getDomain(options.domain), function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
+20 -7
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
setFqdn: setFqdn,
token: token,
version: version,
setVersion: setVersion,
@@ -31,6 +32,7 @@ exports = module.exports = {
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
setZoneName: setZoneName,
isDemo: isDemo,
@@ -46,6 +48,7 @@ var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
@@ -74,6 +77,7 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
data.zoneName = '';
data.token = null;
data.version = null;
@@ -143,10 +147,26 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setFqdn(fqdn) {
set('fqdn', fqdn);
}
function fqdn() {
return get('fqdn');
}
function setZoneName(zone) {
set('zoneName', zone);
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
}
// keep this in sync with start.sh admin.conf generation code
function appFqdn(location) {
assert.strictEqual(typeof location, 'string');
@@ -191,13 +211,6 @@ function isCustomDomain() {
return get('isCustomDomain');
}
function zoneName() {
if (isCustomDomain()) return fqdn(); // the appstore sets up the custom domain as a zone
// for shared domain name, strip out the hostname
return fqdn().substr(fqdn().indexOf('.') + 1);
}
function database() {
return get('database');
}
+22 -9
View File
@@ -15,6 +15,7 @@ var apps = require('./apps.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
digest = require('./digest.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
@@ -22,20 +23,21 @@ var apps = require('./apps.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gAutoupdaterJob = null,
gBoxUpdateCheckerJob = null,
var gAliveJob = null, // send periodic stats
gAppUpdateCheckerJob = null,
gHeartbeatJob = null, // for CaaS health check
gAliveJob = null, // send periodic stats
gAutoupdaterJob = null,
gBackupJob = null,
gCleanupTokensJob = null,
gCleanupBackupsJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gBoxUpdateCheckerJob = null,
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null,
gCleanupBackupsJob = null,
gCleanupEventlogJob = null,
gDynamicDNSJob = null;
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gDynamicDNSJob = null,
gHeartbeatJob = null, // for CaaS health check
gSchedulerSyncJob = null,
gDigestEmailJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
@@ -173,6 +175,14 @@ function recreateJobs(tz) {
start: true,
timeZone: tz
});
if (gDigestEmailJob) gDigestEmailJob.stop();
gDigestEmailJob = new CronJob({
cronTime: '00 00 00 * * 3', // every wednesday
onTick: digest.maybeSend,
start: true,
timeZone: tz
});
}
function autoupdatePatternChanged(pattern) {
@@ -272,5 +282,8 @@ function uninitialize(callback) {
if (gDynamicDNSJob) gDynamicDNSJob.stop();
gDynamicDNSJob = null;
if (gDigestEmailJob) gDigestEmailJob.stop();
gDigestEmailJob = null;
callback();
}
+64
View File
@@ -0,0 +1,64 @@
'use strict';
var appstore = require('./appstore.js'),
debug = require('debug')('box:digest'),
eventlog = require('./eventlog.js'),
updatechecker = require('./updatechecker.js'),
mailer = require('./mailer.js'),
settings = require('./settings.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
exports = module.exports = {
maybeSend: maybeSend
};
function maybeSend(callback) {
callback = callback || NOOP_CALLBACK;
settings.getEmailDigest(function (error, enabled) {
if (error) return callback(error);
if (!enabled) {
debug('Email digest is disabled');
return callback();
}
var updateInfo = updatechecker.getUpdateInfo();
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
if (error) debug('Error getting subscription:', error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
if (error) return callback(error);
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
if (error) return callback(error);
var info = {
hasSubscription: hasSubscription,
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
};
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');
}
callback();
});
});
});
});
}
+2 -1
View File
@@ -112,9 +112,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
+7 -4
View File
@@ -180,9 +180,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -193,7 +194,7 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(domain, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
@@ -202,7 +203,9 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
+2 -1
View File
@@ -56,9 +56,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
callback(new Error('not implemented'));
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
+3 -2
View File
@@ -51,15 +51,16 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
dns.resolveNs(domain, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
async.every(nameservers, function (nameserver, everyNsCallback) {
+4 -2
View File
@@ -46,8 +46,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, value, type, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
@@ -56,9 +57,10 @@ function waitForDns(domain, value, type, options, callback) {
callback();
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
+8 -5
View File
@@ -218,9 +218,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -234,11 +235,11 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(domain, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getHostedZone(credentials, domain, function (error, zone) {
getHostedZone(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) {
@@ -246,7 +247,9 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
+4 -4
View File
@@ -8,7 +8,6 @@ var assert = require('assert'),
dig = require('../dig.js'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
tld = require('tldjs'),
util = require('util');
function isChangeSynced(domain, value, type, nameserver, callback) {
@@ -38,7 +37,7 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
}
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, answer);
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
@@ -60,18 +59,19 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, value, type, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
var zoneName = tld.getDomain(domain);
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
+12
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
add: add,
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
@@ -103,6 +104,17 @@ function getAllPaged(action, search, page, perPage, callback) {
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
assert.strictEqual(typeof callback, 'function');
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
});
}
function cleanup(callback) {
callback = callback || NOOP_CALLBACK;
+15
View File
@@ -3,6 +3,7 @@
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
add: add,
count: count,
delByCreationTime: delByCreationTime,
@@ -71,6 +72,20 @@ function getAllPaged(action, search, page, perPage, callback) {
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
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) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
+1 -1
View File
@@ -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.32.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.33.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
+47
View File
@@ -0,0 +1,47 @@
<% if (format === 'text') { -%>
Dear <%= cloudronName %> Admin,
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
<% if (info.pendingBoxUpdate) { -%>
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { -%>
* <%- info.pendingBoxUpdate.changelog[i] %>
<% }} -%>
<% if (info.pendingAppUpdates.length) { -%>
One or more app updates are available:
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { -%>
- <%= info.pendingAppUpdates[i].manifest.title %> package v<%= info.pendingAppUpdates[i].manifest.version %>
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { -%>
<%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j] %>
<% }}} -%>
<% if (info.finishedBoxUpdates.length) { -%>
Cloudron was updated with the following releases:
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { -%>
- Version <%= info.finishedBoxUpdates[i].boxUpdateInfo.version %>
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { -%>
* <%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j] %>
<% }}} -%>
<% if (info.finishedAppUpdates.length) { -%>
The following apps were updated:
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { -%>
- <%= info.finishedAppUpdates[i].toManifest.title %> package v<%= info.finishedAppUpdates[i].toManifest.version %>
<% 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.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 { %>
<% } %>
+25
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
passwordReset: passwordReset,
boxUpdateAvailable: boxUpdateAvailable,
appUpdateAvailable: appUpdateAvailable,
sendDigest: sendDigest,
sendInvite: sendInvite,
unexpectedExit: unexpectedExit,
@@ -418,6 +419,30 @@ function appUpdateAvailable(app, updateInfo) {
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), cloudronName: cloudronName, info: info, format: 'text' })
};
enqueue(mailOptions);
});
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
+11 -5
View File
@@ -239,16 +239,22 @@ function createMailConfig(callback) {
const mailFqdn = config.adminFqdn();
const alertsFrom = 'no-reply@' + config.fqdn();
debug('createMailConfig: generating mail config');
user.getOwner(function (error, owner) {
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(',');
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
settings.getCatchAllAddress(function (error, address) {
var catchAll = address.join(',');
callback();
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}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback();
});
});
}
+1 -1
View File
@@ -139,7 +139,7 @@ function installApp(req, res, next) {
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
+5 -1
View File
@@ -80,7 +80,9 @@ function dnsSetup(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
cloudron.dnsSetup(req.body, req.body.domain.toLowerCase(), function (error) {
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
cloudron.dnsSetup(req.body, req.body.domain.toLowerCase(), req.body.zoneName || '', function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
@@ -159,6 +161,8 @@ function migrate(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
}
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
var options = _.pick(req.body, 'domain', 'size', 'region');
+29 -1
View File
@@ -24,6 +24,9 @@ exports = module.exports = {
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getCatchAllAddress: getCatchAllAddress,
setCatchAllAddress: setCatchAllAddress,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig,
@@ -125,6 +128,31 @@ function setMailConfig(req, res, next) {
});
}
function getCatchAllAddress(req, res, next) {
settings.getCatchAllAddress(function (error, address) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { address: address }));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
settings.setCatchAllAddress(req.body.address, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -171,7 +199,7 @@ function setDnsConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
settings.setDnsConfig(req.body, config.fqdn(), function (error) {
settings.setDnsConfig(req.body, config.fqdn(), config.zoneName(), function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
+4 -3
View File
@@ -149,7 +149,8 @@ function startBox(done) {
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
config.set('fqdn', 'foobar.com');
config.setFqdn('foobar.com');
config.setZoneName('foobar.com');
awsHostedZones = {
HostedZones: [{
@@ -231,7 +232,7 @@ function startBox(done) {
}, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], function (error) {
@@ -640,7 +641,7 @@ describe('App installation', function () {
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
+1 -1
View File
@@ -28,7 +28,7 @@ function setup(done) {
nock.cleanAll();
config._reset();
config.setVersion('1.2.3');
config.set('fqdn', 'localhost');
config.setFqdn('localhost');
async.series([
server.start.bind(server),
+1 -1
View File
@@ -27,7 +27,7 @@ function setup(done) {
nock.cleanAll();
config._reset();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
config.setFqdn('localhost');
server.start(function (error) {
if (error) return done(error);
+52 -1
View File
@@ -28,7 +28,7 @@ var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
config.setFqdn('foobar.com');
async.series([
server.start.bind(server),
@@ -315,6 +315,57 @@ describe('Settings API', function () {
});
});
describe('catch_all', function () {
it('get catch_all succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/catch_all_address')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.eql({ address: [ ] });
done();
});
});
it('cannot set without address field', function (done) {
superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set with bad address field', function (done) {
superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address')
.query({ access_token: token })
.send({ address: [ "user1", 123 ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address')
.query({ access_token: token })
.send({ address: [ "user1" ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/catch_all_address')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.eql({ address: [ "user1" ] });
done();
});
});
});
describe('Certificates API', function () {
var validCert0, validKey0, // foobar.com
validCert1, validKey1; // *.foobar.com
+1 -1
View File
@@ -28,7 +28,7 @@ var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
config.setFqdn('foobar.com');
async.series([
server.start.bind(server),
+2
View File
@@ -211,6 +211,8 @@ function initializeExpressSync() {
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig);
router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig);
router.get ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.getCatchAllAddress);
router.put ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.setCatchAllAddress);
// feedback
router.post('/api/v1/feedback', usersScope, routes.cloudron.feedback);
+64 -2
View File
@@ -44,13 +44,20 @@ exports = module.exports = {
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
setCatchAllAddress: setCatchAllAddress,
getCatchAllAddress: getCatchAllAddress,
getDefaultSync: getDefaultSync,
getEmailDigest: getEmailDigest,
setEmailDigest: setEmailDigest,
getAll: getAll,
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode',
EMAIL_DIGEST: 'email_digest',
DNS_CONFIG_KEY: 'dns_config',
DYNAMIC_DNS_KEY: 'dynamic_dns',
BACKUP_CONFIG_KEY: 'backup_config',
@@ -58,6 +65,7 @@ exports = module.exports = {
UPDATE_CONFIG_KEY: 'update_config',
APPSTORE_CONFIG_KEY: 'appstore_config',
MAIL_CONFIG_KEY: 'mail_config',
CATCH_ALL_ADDRESS: 'catch_all_address',
events: null
};
@@ -77,6 +85,7 @@ var assert = require('assert'),
moment = require('moment-timezone'),
net = require('net'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
subdomains = require('./subdomains.js'),
@@ -104,6 +113,8 @@ var gDefaults = (function () {
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
result[exports.APPSTORE_CONFIG_KEY] = {};
result[exports.MAIL_CONFIG_KEY] = { enabled: false };
result[exports.CATCH_ALL_ADDRESS] = [ ];
result[exports.EMAIL_DIGEST] = true;
return result;
})();
@@ -492,15 +503,16 @@ function getDnsConfig(callback) {
});
}
function setDnsConfig(dnsConfig, domain, callback) {
function setDnsConfig(dnsConfig, domain, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
subdomains.verifyDnsConfig(dnsConfig, domain, ip, function (error, result) {
subdomains.verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record:' + error.message));
@@ -653,6 +665,56 @@ function setMailConfig(mailConfig, callback) {
});
}
function getEmailDigest(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.EMAIL_DIGEST, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.EMAIL_DIGEST]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, !!enabled); // settingsdb holds string values only
});
}
function setEmailDigest(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.EMAIL_DIGEST, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.EMAIL_DIGEST, enabled);
callback(null);
});
}
function getCatchAllAddress(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CATCH_ALL_ADDRESS, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CATCH_ALL_ADDRESS]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setCatchAllAddress(address, callback) {
assert(Array.isArray(address));
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CATCH_ALL_ADDRESS, JSON.stringify(address), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.CATCH_ALL_ADDRESS, address);
platform.createMailConfig(NOOP_CALLBACK);
callback(null);
});
}
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
+24 -6
View File
@@ -13,6 +13,7 @@ module.exports = exports = {
var assert = require('assert'),
config = require('./config.js'),
settings = require('./settings.js'),
tld = require('tldjs'),
util = require('util');
function SubdomainError(reason, errorOrMessage) {
@@ -58,6 +59,17 @@ function api(provider) {
}
}
function getName(subdomain) {
// support special caas domains
if (!config.isCustomDomain()) return subdomain;
if (config.fqdn() === config.zoneName()) return subdomain;
var part = config.fqdn().slice(0, -config.zoneName().length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
}
function get(subdomain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -66,7 +78,7 @@ function get(subdomain, type, callback) {
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).get(dnsConfig, config.zoneName(), subdomain, type, function (error, values) {
api(dnsConfig.provider).get(dnsConfig, config.zoneName(), getName(subdomain), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -83,7 +95,7 @@ function upsert(subdomain, type, values, callback) {
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).upsert(dnsConfig, config.zoneName(), subdomain, type, values, function (error, changeId) {
api(dnsConfig.provider).upsert(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
@@ -100,7 +112,7 @@ function remove(subdomain, type, values, callback) {
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).del(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
api(dnsConfig.provider).del(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
callback(null);
@@ -118,19 +130,25 @@ function waitForDns(domain, value, type, options, callback) {
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).waitForDns(domain, value, type, options, callback);
var zoneName = config.zoneName();
// if the domain is on another zone in case of external domain, use the correct zone
if (!domain.endsWith(zoneName)) zoneName = tld.getDomain(domain);
api(dnsConfig.provider).waitForDns(domain, zoneName, value, type, options, callback);
});
}
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
assert(typeof dnsConfig.provider === 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var backend = api(dnsConfig.provider);
if (!backend) return callback(new SubdomainError(SubdomainError.INVALID_PROVIDER));
api(dnsConfig.provider).verifyDnsConfig(dnsConfig, domain, ip, callback);
api(dnsConfig.provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback);
}
+3 -2
View File
@@ -68,7 +68,8 @@ var APP = {
describe('apptask', function () {
before(function (done) {
config.set('version', '0.5.0');
config.set('fqdn', 'foobar.com');
config.setFqdn('foobar.com');
config.setZoneName('foobar.com');
config.set('provider', 'caas');
awsHostedZones = {
@@ -90,7 +91,7 @@ describe('apptask', function () {
database.initialize,
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP),
settings.initialize,
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
});
+3 -4
View File
@@ -41,7 +41,7 @@ describe('config', function () {
expect(config.fqdn()).to.equal('localhost');
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '.localhost');
expect(config.appFqdn('app')).to.equal('app.localhost');
expect(config.zoneName()).to.equal('localhost');
expect(config.zoneName()).to.equal('');
});
it('set saves value in file', function (done) {
@@ -63,7 +63,7 @@ describe('config', function () {
});
it('uses dotted locations with custom domain', function () {
config.set('fqdn', 'example.com');
config.setFqdn('example.com');
config.set('isCustomDomain', true);
expect(config.isCustomDomain()).to.equal(true);
@@ -74,7 +74,7 @@ describe('config', function () {
});
it('uses hyphen locations with non-custom domain', function () {
config.set('fqdn', 'test.example.com');
config.setFqdn('test.example.com');
config.set('isCustomDomain', false);
expect(config.isCustomDomain()).to.equal(false);
@@ -95,4 +95,3 @@ describe('config', function () {
});
});
+28 -18
View File
@@ -18,10 +18,11 @@ var async = require('async'),
describe('dns provider', function () {
before(function (done) {
config._reset();
async.series([
database.initialize,
settings.initialize,
config._reset
settings.initialize
], done);
});
@@ -35,7 +36,10 @@ describe('dns provider', function () {
provider: 'noop'
};
settings.setDnsConfig(data, config.fqdn(), done);
config.setFqdn('example.com');
config.setZoneName('example.com');
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
});
it('upsert succeeds', function (done) {
@@ -76,7 +80,10 @@ describe('dns provider', function () {
token: TOKEN
};
settings.setDnsConfig(data, config.fqdn(), done);
config.setFqdn('example.com');
config.setZoneName('example.com');
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
});
it('upsert non-existing record succeeds', function (done) {
@@ -93,10 +100,10 @@ describe('dns provider', function () {
};
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.get('/v2/domains/localhost/records')
.get('/v2/domains/' + config.zoneName() + '/records')
.reply(200, { domain_records: [] });
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.post('/v2/domains/localhost/records')
.post('/v2/domains/' + config.zoneName() + '/records')
.reply(201, { domain_record: DOMAIN_RECORD_0 });
subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) {
@@ -143,10 +150,10 @@ describe('dns provider', function () {
};
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.get('/v2/domains/localhost/records')
.get('/v2/domains/' + config.zoneName() + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
.reply(200, { domain_records: DOMAIN_RECORD_1_NEW });
subdomains.upsert('test', 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
@@ -223,16 +230,16 @@ describe('dns provider', function () {
};
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.get('/v2/domains/localhost/records')
.get('/v2/domains/' + config.zoneName() + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2 ] });
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
.reply(200, { domain_records: DOMAIN_RECORD_1_NEW });
var req3 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_2.id)
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_2.id)
.reply(200, { domain_records: DOMAIN_RECORD_2_NEW });
var req4 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.post('/v2/domains/localhost/records')
.post('/v2/domains/' + config.zoneName() + '/records')
.reply(201, { domain_records: DOMAIN_RECORD_2_NEW });
subdomains.upsert('', 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
@@ -271,7 +278,7 @@ describe('dns provider', function () {
};
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.get('/v2/domains/localhost/records')
.get('/v2/domains/' + config.zoneName() + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
subdomains.get('test', 'A', function (error, result) {
@@ -309,10 +316,10 @@ describe('dns provider', function () {
};
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.get('/v2/domains/localhost/records')
.get('/v2/domains/' + config.zoneName() + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
.delete('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
.delete('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
.reply(204, {});
subdomains.remove('test', 'A', ['1.2.3.4'], function (error) {
@@ -326,13 +333,16 @@ describe('dns provider', function () {
});
describe('route53', function () {
config.setFqdn('example.com');
config.setZoneName('example.com');
// do not clear this with [] but .length = 0 so we don't loose the reference in mockery
var awsAnswerQueue = [];
var AWS_HOSTED_ZONES = {
HostedZones: [{
Id: '/hostedzone/Z34G16B38TNZ9L',
Name: 'localhost.',
Name: config.zoneName() + '.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
@@ -401,7 +411,7 @@ describe('dns provider', function () {
AWS._originalRoute53 = AWS.Route53;
AWS.Route53 = Route53Mock;
settings.setDnsConfig(data, config.fqdn(), done);
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
});
after(function () {
@@ -470,7 +480,7 @@ describe('dns provider', function () {
awsAnswerQueue.push([null, AWS_HOSTED_ZONES]);
awsAnswerQueue.push([null, {
ResourceRecordSets: [{
Name: 'test.localhost.',
Name: 'test.' + config.zoneName() + '.',
Type: 'A',
ResourceRecords: [{
Value: '1.2.3.4'
+21 -1
View File
@@ -95,7 +95,7 @@ describe('Settings', function () {
});
it('can set dns config', function (done) {
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, config.fqdn(), function (error) {
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, config.fqdn(), config.zoneName(), function (error) {
expect(error).to.be(null);
done();
});
@@ -181,6 +181,26 @@ describe('Settings', function () {
});
});
it('can get catch all address', function (done) {
settings.getCatchAllAddress(function (error, address) {
expect(error).to.be(null);
expect(address).to.eql([ ]);
done();
});
});
it('can set catch all address', function (done) {
settings.setCatchAllAddress([ "user1", "user2" ], function (error) {
expect(error).to.be(null);
settings.getCatchAllAddress(function (error, address) {
expect(error).to.be(null);
expect(address).to.eql([ "user1", "user2" ]);
done();
});
});
});
it('can get all values', function (done) {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
+136 -4
View File
@@ -43,6 +43,7 @@ function checkMails(number, done) {
function cleanup(done) {
mailer._clearMailQueue();
safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE);
async.series([
settings.uninitialize,
@@ -50,7 +51,7 @@ function cleanup(done) {
], done);
}
describe('updatechecker - box - manual (mail)', function () {
describe('updatechecker - box - manual (email)', function () {
before(function (done) {
config._reset();
config.set('version', '1.0.0');
@@ -96,11 +97,17 @@ describe('updatechecker - box - manual (mail)', function () {
.query({ boxVersion: config.version(), accessToken: 'token' })
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'pro' } } } );
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('2.0.0.tar.gz');
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(1, done);
});
@@ -134,10 +141,16 @@ describe('updatechecker - box - manual (mail)', function () {
.query({ boxVersion: config.version(), accessToken: 'token' })
.reply(200, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'pro' } } } );
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-pre.0');
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(1, done);
});
@@ -162,7 +175,7 @@ describe('updatechecker - box - manual (mail)', function () {
});
});
describe('updatechecker - box - automatic', function () {
describe('updatechecker - box - automatic (no email)', function () {
before(function (done) {
config.set('version', '1.0.0');
config.set('apiServerOrigin', 'http://localhost:4444');
@@ -186,17 +199,63 @@ describe('updatechecker - box - automatic', function () {
.query({ boxVersion: config.version(), accessToken: 'token' })
.reply(200, { version: '2.0.0', sourceTarballUrl: '2.0.0.tar.gz' } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'pro' } } } );
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(0, done);
});
});
});
describe('updatechecker - app - manual (mails)', function () {
describe('updatechecker - box - automatic free (email)', function () {
before(function (done) {
config.set('version', '1.0.0');
config.set('apiServerOrigin', 'http://localhost:4444');
config.set('provider', 'notcaas');
async.series([
database.initialize,
settings.initialize,
mailer._clearMailQueue,
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
after(cleanup);
it('new version', function (done) {
nock.cleanAll();
var scope = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
.query({ boxVersion: config.version(), accessToken: 'token' })
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'free' } } } );
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(1, done);
});
});
});
describe('updatechecker - app - manual (email)', function () {
var APP_0 = {
id: 'appid-0',
appStoreId: 'io.cloudron.app',
@@ -282,10 +341,16 @@ describe('updatechecker - app - manual (mails)', function () {
.query({ boxVersion: config.version(), accessToken: 'token', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(200, { manifest: { version: '2.0.0' } } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'pro' } } } );
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0' } } });
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(1, done);
});
@@ -302,7 +367,7 @@ describe('updatechecker - app - manual (mails)', function () {
});
});
describe('updatechecker - app - automatic (no emails)', function () {
describe('updatechecker - app - automatic (no email)', function () {
var APP_0 = {
id: 'appid-0',
appStoreId: 'io.cloudron.app',
@@ -362,3 +427,70 @@ describe('updatechecker - app - automatic (no emails)', function () {
});
});
});
describe('updatechecker - app - automatic free (email)', function () {
var APP_0 = {
id: 'appid-0',
appStoreId: 'io.cloudron.app',
installationState: appdb.ISTATE_PENDING_INSTALL,
installationProgress: null,
runState: null,
location: 'some-location-0',
manifest: {
version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0',
tcpPorts: {
PORT: {
description: 'this is a port that i expose',
containerPort: '1234'
}
}
},
httpPort: null,
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
accessRestriction: null,
memoryLimit: 0
};
before(function (done) {
config.set('version', '1.0.0');
config.set('apiServerOrigin', 'http://localhost:4444');
config.set('provider', 'notcaas');
async.series([
database.initialize,
database._clear,
settings.initialize,
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
after(cleanup);
it('offers new version', function (done) {
nock.cleanAll();
var scope = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/appupdate')
.query({ boxVersion: config.version(), accessToken: 'token', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(200, { manifest: { version: '2.0.0' } } );
var scope2 = nock('http://localhost:4444')
.get('/api/v1/users/uid/cloudrons/cid/subscription')
.query({ accessToken: 'token' })
.reply(200, { subscription: { plan: { id: 'free' } } } );
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0' } } });
expect(scope.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
checkMails(1, done);
});
});
});
+45 -18
View File
@@ -95,19 +95,34 @@ function checkAppUpdates(callback) {
if (oldState[app.id] === newState[app.id]) {
debug('Skipping notification of app update %s since user was already notified', app.id);
} else {
// only send notifications if update pattern is 'never'
settings.getAutoupdatePattern(function (error, result) {
if (error) return debug(error);
if (result !== constants.AUTOUPDATE_PATTERN_NEVER) return;
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
mailer.appUpdateAvailable(app, updateInfo);
});
return iteratorDone();
}
iteratorDone();
appstore.getSubscription(function (error, result) {
if (error) {
debug('Error getting subscription for %s', app.id, error);
return iteratorDone();
}
// 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);
return iteratorDone();
}
// only send notifications if update pattern is 'never'
settings.getAutoupdatePattern(function (error, result) {
if (error) {
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);
}
iteratorDone();
});
});
});
}, function () {
newState.box = loadState().box; // preserve the latest box state information
@@ -147,16 +162,28 @@ function checkBoxUpdates(callback) {
return callback();
}
state.box = updateInfo.version;
appstore.getSubscription(function (error, result) {
if (error) return callback(error);
saveState(state);
function done() {
state.box = updateInfo.version;
saveState(state);
callback();
}
// 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);
// 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);
return done();
}
callback();
// 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);
done();
});
});
});
});
@@ -0,0 +1,354 @@
"use strict";
angular.module("ui.multiselect", ["multiselect.tpl.html"])
//from bootstrap-ui typeahead parser
.factory("optionParser", ["$parse", function($parse) {
// 00000111000000000000022200000000000000003333333333333330000000000044000
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
return {
parse: function(input) {
var match = input.match(TYPEAHEAD_REGEXP);
if(!match) {
throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + " but got '" + input + "'.");
}
return {
itemName : match[3],
source : $parse(match[4]),
viewMapper : $parse(match[2] || match[1]),
modelMapper: $parse(match[1])
};
}
};
}])
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "optionParser", function($parse, $document, $compile, $interpolate, optionParser) {
return {
restrict: "E",
require : "ngModel",
link : function(originalScope, element, attrs, modelCtrl) {
var exp = attrs.options;
var parsedResult = optionParser.parse(exp);
var isMultiple = attrs.multiple ? true : false;
var compareByKey = attrs.compareBy;
var headerKey = attrs.headerKey;
var dividerKey = attrs.dividerKey;
var scrollAfterRows = attrs.scrollAfterRows;
var tabindex = attrs.tabindex;
var maxWidth = attrs.maxWidth;
var required = false;
var scope = originalScope.$new();
scope.filterAfterRows = attrs.filterAfterRows;
var changeHandler = attrs.change || angular.noop;
scope.items = [];
scope.header = "Select";
scope.multiple = isMultiple;
scope.disabled = false;
scope.ulStyle = {};
if(scrollAfterRows !== undefined && parseInt(scrollAfterRows).toString() === scrollAfterRows) {
scope.ulStyle = {"max-height": (scrollAfterRows*26+14)+"px", "overflow-y": "auto", "overflow-x": "hidden"};
}
if(tabindex !== undefined && parseInt(tabindex).toString() === tabindex) {
scope.tabindex = tabindex;
}
if(maxWidth !== undefined && parseInt(maxWidth).toString() === maxWidth) {
scope.maxWidth = {"max-width": maxWidth + "px"};
}
originalScope.$on("$destroy", function() {
scope.$destroy();
});
var popUpEl = angular.element("<multiselect-popup></multiselect-popup>");
//required validator
if(attrs.required || attrs.ngRequired) {
required = true;
}
attrs.$observe("required", function(newVal) {
required = newVal;
});
//watch disabled state
scope.$watch(function() {
return $parse(attrs.ngDisabled)(originalScope);
}, function(newVal) {
scope.disabled = newVal;
});
//watch single/multiple state for dynamically change single to multiple
scope.$watch(function() {
return $parse(attrs.multiple)(originalScope);
}, function(newVal) {
isMultiple = newVal || false;
});
//watch option changes for options that are populated dynamically
scope.$watch(function() {
return parsedResult.source(originalScope);
}, function(newVal) {
if(angular.isDefined(newVal)) {
parseModel();
}
}, true);
//watch model change
scope.$watch(function() {
return modelCtrl.$modelValue;
}, function(newVal, oldVal) {
//when directive initialize, newVal usually undefined. Also, if model value already set in the controller
//for preselected list then we need to mark checked in our scope item. But we don't want to do this every time
//model changes. We need to do this only if it is done outside directive scope, from controller, for example.
if(angular.isDefined(newVal)) {
markChecked(newVal);
scope.$eval(changeHandler);
}
getHeaderText();
modelCtrl.$setValidity("required", scope.valid());
});
function parseModel() {
scope.items.length = 0;
var model = parsedResult.source(originalScope);
if(!angular.isDefined(model) || model === null) {
return;
}
for(var i = 0; i < model.length; i++) {
var local = {};
local[parsedResult.itemName] = model[i];
// calculate checked status of the option
// https://github.com/sebastianha/angular-bootstrap-multiselect/pull/4/files
var id = model[i];
var checked = false;
var modelValue = modelCtrl.$modelValue;
if (modelValue) {
if (angular.isArray(modelValue)) {
for (var j = 0; j < modelValue.length; j++)
if (modelValue[j] == id) {
checked = true;
break;
}
} else {
checked = modelValue == id;
}
}
scope.items.push({
label : parsedResult.viewMapper(local),
model : model[i],
checked: checked,
header : model[i][headerKey],
divider : model[i][dividerKey]
});
}
}
parseModel();
element.append($compile(popUpEl)(scope));
function getHeaderText() {
if(isEmpty(modelCtrl.$modelValue)) {
scope.header = attrs.msHeader || "Select";
return scope.header;
}
if(isMultiple) {
if(attrs.msSelected) {
scope.header = $interpolate(attrs.msSelected)(scope);
} else {
scope.header = modelCtrl.$modelValue.length + " " + "selected";
}
} else {
var local = {};
local[parsedResult.itemName] = modelCtrl.$modelValue;
scope.header = parsedResult.viewMapper(local);
}
}
function isEmpty(obj) {
if(obj === true || obj === false) {
return false;
}
if(!obj) {
return true;
}
if(obj.length && obj.length > 0) {
return false;
}
for(var prop in obj) {
if(obj[prop]) {
return false;
}
}
if(compareByKey !== undefined && obj[compareByKey] !== undefined) {
return false;
}
return true;
}
scope.valid = function validModel() {
if(!required) {
return true;
}
var value = modelCtrl.$modelValue;
return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value !== null);
};
function selectSingle(item) {
if(!item.checked) {
scope.uncheckAll();
item.checked = !item.checked;
}
setModelValue(false);
}
function selectMultiple(item) {
item.checked = !item.checked;
setModelValue(true);
}
function setModelValue(isMultiple) {
var value;
if(isMultiple) {
value = [];
angular.forEach(scope.items, function(item) {
if(item.checked) {
value.push(item.model);
}
});
} else {
angular.forEach(scope.items, function(item) {
if(item.checked) {
value = item.model;
return false;
}
});
}
modelCtrl.$setViewValue(value);
}
function markChecked(newVal) {
if(!angular.isArray(newVal)) {
angular.forEach(scope.items, function(item) {
item.checked = false;
if(compareByKey === undefined && angular.equals(item.model, newVal)) {
item.checked = true;
} else if(compareByKey !== undefined && newVal !== null && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], newVal[compareByKey])) {
item.checked = true;
}
});
} else {
angular.forEach(scope.items, function(item) {
item.checked = false;
angular.forEach(newVal, function(i) {
if(compareByKey === undefined && angular.equals(item.model, i)) {
item.checked = true;
} else if(compareByKey !== undefined && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], i[compareByKey])) {
item.checked = true;
}
});
});
}
}
scope.checkAll = function() {
if(!isMultiple) {
return;
}
angular.forEach(scope.items, function(item) {
item.checked = true;
});
setModelValue(true);
};
scope.uncheckAll = function() {
angular.forEach(scope.items, function(item) {
item.checked = false;
});
setModelValue(true);
};
scope.select = function(event, item) {
if(isMultiple === false) {
selectSingle(item);
scope.toggleSelect();
} else {
event.stopPropagation();
selectMultiple(item);
}
};
}
};
}])
.directive("multiselectPopup", ["$document", function($document) {
return {
restrict : "E",
scope : false,
replace : true,
templateUrl: "multiselect.tpl.html",
link : function(scope, element, attrs) {
scope.isVisible = false;
scope.toggleSelect = function() {
if(element.hasClass("open")) {
scope.filter = "";
element.removeClass("open");
$document.unbind("click", clickHandler);
} else {
scope.filter = "";
element.addClass("open");
$document.bind("click", clickHandler);
}
};
// $("ul.dropdown-menu").on("click", "[data-stopPropagation]", function(e) {
// e.stopPropagation();
// });
function clickHandler(event) {
if(elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) {
return;
}
element.removeClass("open");
$document.unbind("click", clickHandler);
scope.$apply();
}
var elementMatchesAnyInArray = function(element, elementArray) {
for(var i = 0; i < elementArray.length; i++) {
if(element === elementArray[i]) {
return true;
}
}
return false;
};
}
};
}]);
angular.module("multiselect.tpl.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("multiselect.tpl.html",
"<div class=\"btn-group\">\n" +
" <button tabindex=\"{{tabindex}}\" title=\"{{header}}\" type=\"button\" class=\"btn btn-default dropdown-toggle\" ng-click=\"toggleSelect()\" ng-disabled=\"disabled\" ng-class=\"{'error': !valid()}\">\n" +
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
" </button>\n" +
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"Type to filter options\">" +
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
" </a>\n" +
" <span ng-if=\"i.header\">{{i.label}}</span>" +
" </li>\n" +
" </ul>\n" +
"</div>");
}]);
+5 -1
View File
@@ -62,6 +62,10 @@
<script type="text/javascript" src="/3rdparty/bootstrap-slider/bootstrap-slider.min.js"></script>
<script type="text/javascript" src="/3rdparty/bootstrap-slider/slider.js"></script>
<!-- Anugular Multiselect -->
<!-- https://github.com/sebastianha/angular-bootstrap-multiselect -->
<script src="/3rdparty/js/angular-bootstrap-multiselect.js"></script>
<!-- Main Application -->
<script src="js/index.js"></script>
@@ -188,7 +192,7 @@
</li>
<li ng-show="appstoreConfig && !config.update.box && user.admin && (currentSubscription.plan.id === 'undecided' || currentSubscription.plan.id === 'free')">
<a ng-href="" ng-click="showSubscriptionModal()" style="cursor: pointer">
<span class="badge badge-success">Setup automatic updates</span>
<span class="badge badge-success">Setup Subscription</span>
</a>
</li>
<li>
+14
View File
@@ -404,6 +404,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}
};
Client.prototype.setCatchallAddresses = function (addresses, callback) {
put('/api/v1/settings/catch_all_address', { address: addresses }).success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getCatchallAddresses = function (callback) {
get('/api/v1/settings/catch_all_address').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data.address);
}).error(defaultErrorHandler(callback));
};
Client.prototype.setBackupConfig = function (backupConfig, callback) {
post('/api/v1/settings/backup_config', backupConfig).success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
+1 -1
View File
@@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken;
// create main application module
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld']);
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
app.config(['NotificationProvider', function (NotificationProvider) {
NotificationProvider.setOptions({
+2 -2
View File
@@ -154,7 +154,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
});
}
function getSubscription() {
$scope.getSubscription = function () {
Client.getAppstoreConfig(function (error, result) {
if (error) return console.error(error);
@@ -239,7 +239,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if ($scope.user.admin) {
runConfigurationChecks();
if ($scope.config.provider !== 'caas') getSubscription();
if ($scope.config.provider !== 'caas') $scope.getSubscription();
}
});
});
+28 -3
View File
@@ -1,9 +1,17 @@
'use strict';
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ngTld']);
/* global tld */
app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', function ($scope, $http, Client, ngTld) {
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.filter('zoneName', function () {
return function (domain) {
return tld.getDomain(domain);
};
});
app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
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.initialized = false;
@@ -12,6 +20,22 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
$scope.provider = '';
$scope.showDNSSetup = false;
$scope.instanceId = '';
$scope.explicitZone = search.zone || '';
$scope.isDomain = false;
$scope.isSubdomain = false;
$scope.$watch('dnsCredentials.domain', function (newVal) {
if (!newVal) {
$scope.isDomain = false;
$scope.isSubdomain = false;
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
$scope.isDomain = false;
$scope.isSubdomain = false;
} else {
$scope.isDomain = true;
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
}
});
// keep in sync with certs.js
$scope.dnsProvider = [
@@ -38,6 +62,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
var data = {
domain: $scope.dnsCredentials.domain,
zoneName: $scope.explicitZone,
provider: $scope.dnsCredentials.provider,
accessKeyId: $scope.dnsCredentials.accessKeyId,
secretAccessKey: $scope.dnsCredentials.secretAccessKey,
+1 -1
View File
@@ -70,7 +70,7 @@
<input type="text" class="form-control" ng-model="account.displayName" id="inputDisplayName" name="displayName" placeholder="Display Name" required autocomplete="off" autofocus>
</div>
<div ng-show="account.requireEmail" class="form-group" ng-class="{ 'has-error': setupForm.email.$dirty && setupForm.email.$invalid }">
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset.">
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset. A valid email is also required for Let's Encrypt certificates.">
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) || (!setupForm.username.$dirty && error.username) }">
<p ng-show="!setupForm.username.$dirty && error.username">{{ error.username }}</p>
+5 -6
View File
@@ -28,7 +28,6 @@
<!-- Angular directives for tldjs -->
<script src="3rdparty/js/tld.js"></script>
<script src="3rdparty/js/angular-tld.js"></script>
<!-- Setup Application -->
<script src="js/setupdns.js"></script>
@@ -55,11 +54,11 @@
<div class="col-md-10 col-md-offset-1 text-center">
<h1>Cloudron Setup</h1>
<h3>Provide a domain for your Cloudron</h3>
<p>Apps will be installed on subdomains of that domain.</p>
<p>Apps will be installed on subdomains of this domain.</p>
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" check-tld placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
</div>
<p>&nbsp;<span ng-show="dnsCredentialsForm.domain.$error.invalidSubdomain" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
<p ng-show="isSubdomain" class="text-bold">Installing Cloudron on a subdomain requires an enterprise subscription.</p>
</div>
</div>
<div class="row">
@@ -78,14 +77,14 @@
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
<br/>
<span>The domain {{ dnsCredentials.domain }} must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
<span ng-show="isDomain || explicitZone"><b>{{ explicitZone ? explicitZone : (dnsCredentials.domain | zoneName) }}</b> must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
</div>
<!-- DigitalOcean -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
<br/>
<span>The domain {{ dnsCredentials.domain }} must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
<span ng-show="isDomain || explicitZone"><b>{{ explicitZone ? explicitZone : (dnsCredentials.domain | zoneName) }}</b> must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
</div>
<!-- Wildcard -->
+3 -2
View File
@@ -167,8 +167,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.error.location = 'This name is already taken.';
$scope.appInstallForm.location.$setPristine();
$('#appInstallLocationInput').focus();
} else if (error.statusCode === 402) {
$scope.appstoreLogin.show();
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
$scope.appInstall.error.cert = error.message;
$scope.appInstall.certificateFileName = '';
@@ -312,6 +310,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
return;
}
// check subscription right away after login
$scope.$parent.getSubscription();
fetchAppstoreConfig();
});
});
+1 -2
View File
@@ -11,8 +11,7 @@
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
<label class="control-label" for="customDomainId">Domain name</label>
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
<p>&nbsp;<span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain && dnsCredentials.customDomain !== config.fqdn" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" placeholder="example.com" required autofocus>
</div>
<div class="form-group">
+21
View File
@@ -58,6 +58,27 @@
</div>
</div>
<div class="section-header" ng-show="mailConfig.enabled && isPaying">
<div class="text-left">
<h3>Catch-all</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="mailConfig.enabled && isPaying">
<div class="row">
<div class="col-md-12">
Emails sent to non existing addresses will be forwarded to the following accounts:
</div>
</div>
<br/>
<div class="row">
<div class="col-md-6">
<multiselect ng-model="catchall.addresses" options="address for address in catchall.availableAddresses" data-multiple="true"></multiselect>
<button class="btn btn-outline btn-primary" ng-disabled="catchall.busy" ng-click="catchall.submit()"><i class="fa fa-circle-o-notch fa-spin" ng-show="catchall.busy"></i> Save</button>
</div>
</div>
</div>
<div class="section-header" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
<div class="text-left">
<h3>DNS Records</h3>
+60 -1
View File
@@ -1,6 +1,6 @@
'use strict';
angular.module('Application').controller('EmailController', ['$scope', '$location', '$rootScope', 'Client', function ($scope, $location, $rootScope, Client) {
angular.module('Application').controller('EmailController', ['$scope', '$location', '$rootScope', 'Client', 'AppStore', function ($scope, $location, $rootScope, Client, AppStore) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.client = Client;
@@ -17,6 +17,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
{ name: 'PTR', value: 'ptr' }
];
$scope.mailConfig = null;
$scope.users = [];
$scope.isPaying = false;
$scope.showView = function (view) {
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
@@ -28,6 +30,21 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$('.modal').modal('hide');
};
$scope.catchall = {
addresses: [],
busy: false,
submit: function () {
$scope.catchall.busy = true;
Client.setCatchallAddresses($scope.catchall.addresses, function (error) {
if (error) console.error('Unable to add catchall address.', error);
$scope.catchall.busy = false;
});
}
};
$scope.email = {
refreshBusy: false,
@@ -107,9 +124,51 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
});
}
function getUsers() {
Client.getUsers(function (error, result) {
if (error) return console.error('Unable to get user listing.', error);
// only allow users with a Cloudron email address
$scope.catchall.availableAddresses = result.filter(function (u) { return !!u.email; }).map(function (u) { return u.username; });
});
}
function getCatchallAddresses() {
Client.getCatchallAddresses(function (error, result) {
if (error) return console.error('Unable to get catchall address listing.', error);
// dedupe in case to avoid angular breakage
$scope.catchall.addresses = result.filter(function(item, pos, self) {
return self.indexOf(item) == pos;
});
});
}
function getSubscription() {
if ($scope.config.provider === 'caas') {
$scope.isPaying = true;
return;
}
Client.getAppstoreConfig(function (error, result) {
if (error) return console.error(error);
if (!result.token) return;
AppStore.getSubscription(result, function (error, result) {
if (error) return console.error(error);
$scope.isPaying = result.plan.id !== 'free' && result.plan.id !== 'undecided';
});
});
}
Client.onReady(function () {
getMailConfig();
getDnsConfig();
getSubscription();
getUsers();
getCatchallAddresses();
$scope.email.refresh();
});