Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84153793d9 | |||
| 31ad42501b | |||
| 42476e10e1 | |||
| 91e5f850ab | |||
| 6ee383185d | |||
| 2fb02d57ed | |||
| 6b8edbf4f0 | |||
| 4d7f9ba9a5 | |||
| 6d0cdc36b2 | |||
| 79541a68a5 | |||
| 845d386478 | |||
| 8771de5c12 | |||
| 76246b2952 | |||
| f994b68701 | |||
| 77558c823c | |||
| dd6a19ea85 | |||
| 16978f8c1a | |||
| f311c3da1c | |||
| 423e355fd6 | |||
| 8fadb3badc | |||
| 3845065085 | |||
| 801d848908 | |||
| e6eda1283c | |||
| a553755f4a | |||
| cd52459f05 | |||
| 1802201e9e | |||
| 2d72f49261 | |||
| cd42a6c2ea | |||
| 65f949e669 | |||
| f3fec9a33c | |||
| 13182de57f | |||
| c33566b553 | |||
| 4faf247898 | |||
| 9952a986eb | |||
| 40aaffe365 | |||
| 3745e96a6f | |||
| 157ce06f93 | |||
| 822dfb8af5 | |||
| 9ead482dc6 | |||
| 865c0a7aa7 | |||
| c760c42f92 | |||
| ded31b977e | |||
| 4781c4e364 | |||
| 8e123b017e | |||
| 658cbcdab9 | |||
| 0cc980f539 | |||
| da7648fe3f | |||
| 8db1073980 | |||
| f74f17af02 | |||
| 87ca05281d | |||
| 9780f77fa8 | |||
| 0bddd5a2c6 | |||
| 20f2a6e4c6 | |||
| 6d47737de7 | |||
| e8f9552ff9 | |||
| 9c76c5fc27 | |||
| f9d5f92397 | |||
| 3a2a05dfce | |||
| 5a291fa2a4 | |||
| 84d34ec004 | |||
| 63fca38f0b | |||
| e3b2799230 | |||
| 2efe72519e |
@@ -885,5 +885,13 @@
|
||||
* Prevent email view from flickering
|
||||
* Prepare for 1.0
|
||||
|
||||
[0.160.1]
|
||||
* Improved update notification
|
||||
[1.0.0]
|
||||
* Make selfhosting great again
|
||||
|
||||
[1.0.1]
|
||||
* Notification improvements
|
||||
|
||||
[1.1.0]
|
||||
* Add support for email catch-all
|
||||
* Support Cloudrons on subdomains
|
||||
|
||||
|
||||
+30
-2
@@ -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
|
||||
|
||||
@@ -105,6 +105,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "ovh" && \
|
||||
@@ -113,7 +114,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, digitalocean, ec2, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -281,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
|
||||
|
||||
+23
-1
@@ -4,6 +4,8 @@ exports = module.exports = {
|
||||
purchase: purchase,
|
||||
unpurchase: unpurchase,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
getAppUpdate: getAppUpdate,
|
||||
@@ -69,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');
|
||||
@@ -83,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);
|
||||
|
||||
+13
-5
@@ -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,
|
||||
@@ -615,6 +622,7 @@ function updateToLatest(auditSource, callback) {
|
||||
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
|
||||
|
||||
// check if this is just a version number change
|
||||
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
|
||||
@@ -854,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
@@ -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
@@ -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 * * * 3', // every tuesday
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,21 +2,17 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Cloudron Version <%= newBoxVersion %> is now available!
|
||||
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
|
||||
|
||||
Your Cloudron will update to 1.0 once you have selected a plan (https://cloudron.io/pricing.html).
|
||||
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now.
|
||||
This ensures you are running the latest versions of apps and keeps your server secure. All paid
|
||||
plans come with support via email (support@cloudron.io) and live chat (https://chat.cloudron.io).
|
||||
|
||||
You can read more about our pricing changes in our blog at https://cloudron.io/blog/2017-06-06-pricing.html.
|
||||
Visit our pricing page https://cloudron.io/pricing.html for pricing information.
|
||||
|
||||
Visit your Cloudron at <%= webadminUrl %> to perform the update.
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
* <%- changelog[i] %>
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
Cloudron.io team
|
||||
your Cloudron
|
||||
|
||||
<% } else { %>
|
||||
|
||||
@@ -28,22 +24,20 @@ Cloudron.io team
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
Cloudron Version <b><%= newBoxVersion %></b> is now available!
|
||||
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Your Cloudron will update to 1.0 once you have selected a <a href="https://cloudron.io/pricing.html">plan</a>.
|
||||
</p>
|
||||
<p>
|
||||
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can read more about our pricing changes in our <a href="https://cloudron.io/blog/2017-06-06-pricing.html" target="_blank">blog</a>.
|
||||
Your Cloudron will update automatically tonight.<br/>
|
||||
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Visit your Cloudron <a href="<%= webadminUrl %>">here</a> to perform the update.
|
||||
</p>
|
||||
<h5>Changelog:</h5>
|
||||
<ul>
|
||||
<% for (var i = 0; i < changelogHTML.length; i++) { %>
|
||||
<li><%- changelogHTML[i] %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -58,3 +52,4 @@ Cloudron.io team
|
||||
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
@@ -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 { %>
|
||||
|
||||
<% } %>
|
||||
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CATCH_ALL_ADDRESS_KEY]);
|
||||
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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+50
-25
@@ -27,7 +27,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function loadState() {
|
||||
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
|
||||
return state || {};
|
||||
return state || { };
|
||||
}
|
||||
|
||||
function saveState(mailedUser) {
|
||||
@@ -95,25 +95,37 @@ 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 () {
|
||||
// preserve the latest box state information
|
||||
newState.box = loadState().box;
|
||||
newState.boxTimestamp = loadState().boxTimestamp;
|
||||
|
||||
newState.box = loadState().box; // preserve the latest box state information
|
||||
saveState(newState);
|
||||
callback();
|
||||
});
|
||||
@@ -145,21 +157,34 @@ function checkBoxUpdates(callback) {
|
||||
// decide whether to send email
|
||||
var state = loadState();
|
||||
|
||||
const NOTIFICATION_OFFSET = 1000 * 60 * 60 * 24 * 5; // 5 days
|
||||
|
||||
if (state.box === gBoxUpdateInfo.version && state.boxTimestamp > Date.now() - NOTIFICATION_OFFSET) {
|
||||
debug('Skipping notification of box update as user was already notified within the last 5 days');
|
||||
if (state.box === gBoxUpdateInfo.version) {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
return callback();
|
||||
}
|
||||
|
||||
state.boxTimestamp = Date.now();
|
||||
state.box = updateInfo.version;
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
function done() {
|
||||
state.box = updateInfo.version;
|
||||
saveState(state);
|
||||
callback();
|
||||
}
|
||||
|
||||
saveState(state);
|
||||
// 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>");
|
||||
}]);
|
||||
+12
-30
@@ -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>
|
||||
|
||||
@@ -148,42 +152,15 @@
|
||||
<h4 class="modal-title" id="updateModalLabel">Setup Subscription</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
You can update to the next version once you have selected a <a ng-href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">plan</a>.
|
||||
<p ng-show="config.update.box">
|
||||
You can update to the next version once you have selected a <a ng-href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">paid plan</a>.
|
||||
</p>
|
||||
<p>
|
||||
With a paid plan, you get continuous updates for the Cloudron and apps. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-success" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email }}" target="_blank">Setup Subscription</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal version 1.0 -->
|
||||
<div class="modal fade" id="version1Modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<h3>Cloudron 1.0 is here!</h3>
|
||||
<p>
|
||||
Your Cloudron will update to 1.0 once you have selected a <a ng-href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">plan</a>.
|
||||
</p>
|
||||
<p>
|
||||
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
|
||||
</p>
|
||||
<p>
|
||||
With the free plan, you can keep the Cloudron and Apps updated on your own
|
||||
following the instructions in our <a href="https://git.cloudron.io/cloudron/box/wikis/home" target="_blank">wiki</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can read more about our pricing changes in our <a href="https://cloudron.io/blog/2017-06-06-pricing.html" target="_blank">blog</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank"><i class="fa fa-circle-o-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
|
||||
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-disabled="waitingForPlanSelection"><i class="fa fa-circle-o-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,6 +190,11 @@
|
||||
<span class="badge badge-success">Update available</span>
|
||||
</a>
|
||||
</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 Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-cloud-download fa-fw"></i> My Apps</a>
|
||||
</li>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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({
|
||||
|
||||
+28
-21
@@ -33,8 +33,15 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
};
|
||||
|
||||
$scope.waitingForPlanSelection = false;
|
||||
$('#version1Modal').on('hide.bs.modal', function () {
|
||||
$('#setupSubscriptionModal').on('hide.bs.modal', function () {
|
||||
$scope.waitingForPlanSelection = false;
|
||||
|
||||
// check for updates to stay in sync
|
||||
Client.checkForUpdates(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
Client.refreshConfig();
|
||||
});
|
||||
});
|
||||
|
||||
$scope.waitForPlanSelection = function () {
|
||||
@@ -45,18 +52,29 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
function checkPlan() {
|
||||
if (!$scope.waitingForPlanSelection) return;
|
||||
|
||||
if ($scope.currentSubscription.plan.id !== 'undecided') {
|
||||
$scope.waitingForPlanSelection = false;
|
||||
$('#version1Modal').modal('hide');
|
||||
$('#updateModal').modal('show');
|
||||
} else {
|
||||
$timeout(checkPlan, 1000);
|
||||
}
|
||||
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.currentSubscription = result;
|
||||
|
||||
// check again to give more immediate feedback once a subscription was setup
|
||||
if (result.plan.id === 'undecided' || result.plan.id === 'free') {
|
||||
$timeout(checkPlan, 5000);
|
||||
} else {
|
||||
$scope.waitingForPlanSelection = false;
|
||||
$('#setupSubscriptionModal').modal('hide');
|
||||
if ($scope.config.update && $scope.config.update.box) $('#updateModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkPlan();
|
||||
};
|
||||
|
||||
$scope.showSubscriptionModal = function () {
|
||||
$('#setupSubscriptionModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showUpdateModal = function (form) {
|
||||
$scope.update.error.generic = null;
|
||||
$scope.update.error.password = null;
|
||||
@@ -73,14 +91,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
$('#updateModal').modal('show');
|
||||
} else if (!$scope.currentSubscription || !$scope.currentSubscription.plan) {
|
||||
// do nothing as we were not able to get a subscription, yet
|
||||
} else if ($scope.config.update.box.version === '1.0.0') {
|
||||
// special case for updating to 1.0
|
||||
if ($scope.currentSubscription.plan.id === 'undecided') {
|
||||
$('#version1Modal').modal('show');
|
||||
} else {
|
||||
// user selected a plan already, let him update
|
||||
$('#updateModal').modal('show');
|
||||
}
|
||||
} else {
|
||||
$('#updateModal').modal('show');
|
||||
}
|
||||
@@ -144,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);
|
||||
|
||||
@@ -160,9 +170,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.currentSubscription = result;
|
||||
|
||||
// check again to give more immediate feedback once a subscription was setup
|
||||
if (result.plan.id === 'undecided') $timeout(getSubscription, 5000);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -232,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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> <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>{{ dnsCredentials.domain || 'The 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>{{ dnsCredentials.domain || 'The 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 -->
|
||||
|
||||
@@ -153,9 +153,14 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">App Feedback</h4>
|
||||
<h4 class="modal-title">Missing App Feedback</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Please see all previously requested apps <a href="https://git.cloudron.io/cloudron/app-requests/issues" target="_blank">here</a> first.
|
||||
If an app was already requested, leave a comment or give a +1, to help us prioritize better.
|
||||
</p>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form name="feedbackForm" ng-submit="feedback.submit()">
|
||||
<div ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</div>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> <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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -221,23 +221,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal subscription required -->
|
||||
<div class="modal" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The Cloudron Email server is only available in the paid plans.<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="section-header">
|
||||
@@ -323,9 +306,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row" ng-show="currentSubscription.plan.id === 'free'">
|
||||
<div class="row" ng-show="currentSubscription.plan.id === 'free' || currentSubscription.plan.id === 'undecided'">
|
||||
<div class="col-xs-12">
|
||||
A cloudron.io subscription will provide you with effortless automatic app and platform updates.
|
||||
With a paid plan, you get continuous updates for the Cloudron and apps. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -348,8 +331,8 @@
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<a class="btn btn-primary pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="currentSubscription.plan && currentSubscription.plan.id !== 'free'">Change Subscription</a>
|
||||
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="currentSubscription.plan.id === 'free'">Setup Subscription</a>
|
||||
<a class="btn btn-primary pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email }}" target="_blank" ng-show="currentSubscription.plan && currentSubscription.plan.id !== 'free' && currentSubscription.plan.id !== 'undecided'">Configure</a>
|
||||
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="currentSubscription.plan.id === 'free' || currentSubscription.plan.id === 'undecided'">Setup Subscription</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user