Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61f7c1af48 | |||
| 00786dda05 | |||
| 8b9f44addc | |||
| 56c7dbb6e4 | |||
| c47f878203 | |||
| 8a2107e6eb | |||
| cd9f0f69d8 | |||
| 1da91b64f6 | |||
| a87dd65c1d | |||
| 7c63d9e758 | |||
| 329bf596ac | |||
| 2a57c4269a | |||
| ca8813dce3 | |||
| 3aebf51360 | |||
| 103f8db8cb | |||
| 04c127b78d | |||
| 9bef1bcf64 | |||
| 718413c089 | |||
| a34691df44 | |||
| 795e38fe82 | |||
| 1d348fb0f3 | |||
| 91f3318879 | |||
| c61808f4c6 | |||
| 991b2dad28 | |||
| f3d9a70de7 | |||
| 60758de10a | |||
| 6a0ef7a1c1 | |||
| 7cb451c157 | |||
| 3c31c96ad4 | |||
| 5d73f58631 | |||
| 4ca7cccdae | |||
| 82380b6b7c | |||
| 979c4e77e3 | |||
| e318fb0c01 | |||
| 77d2fb97e5 | |||
| 24e6c4d963 | |||
| 064c5cf7f2 | |||
| 891542bfb9 | |||
| 599702d410 | |||
| 3cb39754fd | |||
| f04345a99a | |||
| 3d59b8a5b0 | |||
| cf518b0285 | |||
| 52832c881a | |||
| 537fbff4aa | |||
| e3040b334d | |||
| 6c2879d567 | |||
| 595c89076f | |||
| c85f5b15c6 | |||
| 8fbed7e84b | |||
| ee3c5f67af | |||
| 52db28e876 | |||
| 65bc3491f6 | |||
| 82f512dc27 | |||
| 4b41378d08 | |||
| 1fd4e27d92 | |||
| 2420fef6b1 | |||
| 50074b936a | |||
| f98e68edc1 | |||
| 83e5daf08c | |||
| 53b43ca36b | |||
| d11842a7f8 | |||
| 6746781b46 | |||
| 78ec8e5c0c | |||
| 67a2ba957e | |||
| 9e558924bb | |||
| afcb3dd237 | |||
| 054de4813d | |||
| 57891c64b5 | |||
| 26361c037d | |||
| 2048b03431 | |||
| c12aba6c00 | |||
| 0bd0857189 | |||
| 978893250f | |||
| d0f4a76ca2 |
@@ -948,3 +948,46 @@
|
||||
* Add a custom graphite plugin to collect disk usage statistics
|
||||
* Rotate logs of all apps automatically
|
||||
|
||||
[1.6.0]
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.1]
|
||||
* Patch release for 1.6.0 to fix regressions
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.2]
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.3]
|
||||
* Fixes selection issue while clicking on empty flexbox space
|
||||
* Indicate directories can be downloaded in the web terminal
|
||||
* Do not show app update indicator for normal users
|
||||
* Display email notice when using Cloudflare DNS
|
||||
* Set MX records correctly when using Cloudflare DNS
|
||||
* Fix bug where webterminal can incorrectly appear in main view
|
||||
* Do not crash if DNS credentials are invalid
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ apt-get -y install \
|
||||
rcconf \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN enableBackup BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN enableBackup', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
robotsTxt TEXT,
|
||||
enableBackup BOOLEAN DEFAULT 1,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask
|
||||
|
||||
Generated
+559
-2808
File diff suppressed because it is too large
Load Diff
+26
-25
@@ -1,38 +1,39 @@
|
||||
{
|
||||
"name": "Cloudron",
|
||||
"name": "cloudron",
|
||||
"description": "Main code for a cloudron",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Cloudron authors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "https://git.cloudron.io/cloudron/box.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"engines": [
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.1.4",
|
||||
"aws-sdk": "^2.41.0",
|
||||
"body-parser": "^1.13.1",
|
||||
"cloudron-manifestformat": "^2.8.0",
|
||||
"async": "^2.5.0",
|
||||
"aws-sdk": "^2.97.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"cloudron-manifestformat": "^2.9.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^0.1.0",
|
||||
"connect-timeout": "^1.5.0",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.1.0",
|
||||
"cron": "^1.0.9",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.10.0-beta.20",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^2.2.0",
|
||||
"debug": "^3.0.0",
|
||||
"dockerode": "^2.4.3",
|
||||
"ejs": "^2.2.4",
|
||||
"ejs-cli": "^1.2.0",
|
||||
"express": "^4.12.4",
|
||||
"express-session": "^1.11.3",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.15.4",
|
||||
"express-session": "^1.15.5",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
|
||||
@@ -43,7 +44,6 @@
|
||||
"morgan": "^1.7.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodemailer": "^4.0.1",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.0.1",
|
||||
@@ -62,20 +62,21 @@
|
||||
"semver": "^4.3.6",
|
||||
"showdown": "^1.6.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^1.8.3",
|
||||
"superagent": "^3.5.2",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.15.2.tgz",
|
||||
"tar-fs": "^1.15.3",
|
||||
"tldjs": "^1.6.2",
|
||||
"underscore": "^1.7.0",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^4.9.0"
|
||||
"validator": "^4.9.0",
|
||||
"ws": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"deep-extend": "^0.4.1",
|
||||
"del": "^1.1.1",
|
||||
"expect.js": "*",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^2.3.0",
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
@@ -89,8 +90,8 @@
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"mocha": "*",
|
||||
"mock-aws-s3": "^2.4.0",
|
||||
"nock": "^9.0.2",
|
||||
"node-sass": "^3.0.0-alpha.0",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^3.13.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"request": "^2.65.0",
|
||||
"yargs": "^3.15.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import collectd,os
|
||||
import collectd,os,subprocess
|
||||
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
|
||||
@@ -6,7 +6,7 @@ disks = []
|
||||
|
||||
def init():
|
||||
global disks
|
||||
lines = [s.split() for s in os.popen("df --type=ext4 --output=source,target,size,used,avail").read().splitlines()]
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
|
||||
disks = lines[1:] # strip header
|
||||
collectd.info('custom df plugin initialized with %s' % disks)
|
||||
|
||||
@@ -14,7 +14,7 @@ def read():
|
||||
for d in disks:
|
||||
device = d[0]
|
||||
if 'devicemapper' in d[1] or not device.startswith('/dev/'): continue
|
||||
instance = device[len('/dev/'):].replace('/', '-')
|
||||
instance = device[len('/dev/'):].replace('/', '_') # see #348
|
||||
|
||||
try:
|
||||
st = os.statvfs(d[1]) # handle disk removal
|
||||
|
||||
@@ -80,7 +80,7 @@ server {
|
||||
# No buffering to temp files, it fails for large downloads
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# Disable check to allow unlimited body sizes
|
||||
# Disable check to allow unlimited body sizes. this allows apps to accept whatever size they want
|
||||
client_max_body_size 0;
|
||||
|
||||
<% if (robotsTxtQuoted) { %>
|
||||
@@ -107,6 +107,11 @@ server {
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/apps/.*/upload$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
|
||||
+2
-1
@@ -60,7 +60,7 @@ var assert = require('assert'),
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt' ].join(',');
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -98,6 +98,7 @@ function postProcess(result) {
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
|
||||
+92
-2
@@ -37,6 +37,9 @@ exports = module.exports = {
|
||||
|
||||
getAppConfig: getAppConfig,
|
||||
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
@@ -70,10 +73,11 @@ var addons = require('./addons.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
validator = require('validator');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
@@ -415,6 +419,7 @@ function install(data, auditSource, callback) {
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null;
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
@@ -484,7 +489,8 @@ function install(data, auditSource, callback) {
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
lastBackupId: backupId
|
||||
lastBackupId: backupId,
|
||||
enableBackup: enableBackup
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
@@ -583,6 +589,8 @@ function configure(appId, data, auditSource, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
@@ -1122,3 +1130,85 @@ function configureInstalledApps(callback) {
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(appId, filePath, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (d) { data += d; });
|
||||
stream.on('end', function () {
|
||||
var parts = data.split('-');
|
||||
if (parts.length !== 2) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
|
||||
|
||||
var type = parts[0], filename, cmd, size;
|
||||
|
||||
if (type === 'regular file') {
|
||||
cmd = [ 'cat', filePath ];
|
||||
size = parseInt(parts[1], 10);
|
||||
filename = path.basename(filePath);
|
||||
if (isNaN(size)) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
|
||||
} else if (type === 'directory') {
|
||||
cmd = ['tar', 'zcf', '-', '-C', filePath, '.'];
|
||||
filename = path.basename(filePath) + '.tar.gz';
|
||||
size = 0; // unknown
|
||||
} else {
|
||||
return callback(new AppsError(AppsError.NOT_FOUND, 'only files or dirs can be downloaded'));
|
||||
}
|
||||
|
||||
exec(appId, { cmd: cmd , tty: false }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdoutStream = new TransformStream({
|
||||
transform: function (chunk, ignoredEncoding, callback) {
|
||||
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
||||
|
||||
while (true) {
|
||||
if (this._buffer.length < 8) break; // header is 8 bytes
|
||||
|
||||
var type = this._buffer.readUInt8(0);
|
||||
var len = this._buffer.readUInt32BE(4);
|
||||
|
||||
if (this._buffer.length < (8 + len)) break; // not enough
|
||||
|
||||
var payload = this._buffer.slice(8, 8 + len);
|
||||
|
||||
this._buffer = this._buffer.slice(8+len); // consumed
|
||||
|
||||
if (type === 1) this.push(payload);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
stream.pipe(stdoutStream);
|
||||
|
||||
return callback(null, stdoutStream, { filename: filename, size: size });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof sourceFilePath, 'string');
|
||||
assert.strictEqual(typeof destFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var readFile = fs.createReadStream(sourceFilePath);
|
||||
readFile.on('error', console.error);
|
||||
|
||||
readFile.pipe(stream);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+35
-28
@@ -2,7 +2,9 @@
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize
|
||||
uninitialize: uninitialize,
|
||||
|
||||
accessTokenAuth: accessTokenAuth
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -23,22 +25,22 @@ var assert = require('assert'),
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.id);
|
||||
});
|
||||
|
||||
|
||||
passport.deserializeUser(function(userId, callback) {
|
||||
user.get(userId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
|
||||
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
|
||||
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
user.verifyWithUsername(username, password, function (error, result) {
|
||||
@@ -58,7 +60,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
@@ -80,7 +82,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function(error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
@@ -89,30 +91,35 @@ function initialize(callback) {
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
passport.use(new BearerStrategy(function (accessToken, callback) {
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
|
||||
user.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function accessTokenAuth(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
|
||||
user.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+5
-1
@@ -82,7 +82,6 @@ BackupsError.INTERNAL_ERROR = 'internal error';
|
||||
BackupsError.BAD_STATE = 'bad state';
|
||||
BackupsError.BAD_FIELD = 'bad field';
|
||||
BackupsError.NOT_FOUND = 'not found';
|
||||
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
|
||||
|
||||
// choose which storage backend we use for test purpose we use s3
|
||||
function api(provider) {
|
||||
@@ -370,6 +369,11 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
|
||||
++processed;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
return iteratorCallback(null, app.lastBackupId); // just use the last backup
|
||||
}
|
||||
|
||||
backupApp(app, app.manifest, prefix, function (error, backupId) {
|
||||
if (error && error.reason !== BackupsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ var appdb = require('./appdb.js'),
|
||||
hat = require('hat'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
function ClientsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
|
||||
@@ -109,10 +109,18 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
var i = 0;
|
||||
|
||||
async.eachSeries(values, function (value, callback) {
|
||||
var priority = null;
|
||||
|
||||
if (type === 'MX') {
|
||||
priority = value.split(' ')[0];
|
||||
value = value.split(' ')[1];
|
||||
}
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
name: fqdn,
|
||||
content: value,
|
||||
priority: priority,
|
||||
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
|
||||
};
|
||||
|
||||
|
||||
@@ -209,6 +209,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
SecurityOpt: enableSecurityOpt ? [ "apparmor=docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = manifest.capabilities || [];
|
||||
if (capabilities.includes('net_admin')) {
|
||||
containerOptions.HostConfig.CapAdd = [
|
||||
'NET_ADMIN'
|
||||
];
|
||||
}
|
||||
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:eventlog'),
|
||||
eventlogdb = require('./eventlogdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
|
||||
@@ -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.36.2' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.36.3' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+2
-3
@@ -70,14 +70,13 @@ function cleanupTmpVolume(containerInfo, callback) {
|
||||
docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
|
||||
if (error) return callback(new Error('Failed to exec container : ' + error.message));
|
||||
|
||||
execContainer.start(function(err, stream) {
|
||||
execContainer.start({ hijack: true }, function (error, stream) {
|
||||
if (error) return callback(new Error('Failed to start exec container : ' + error.message));
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', callback);
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.pipe(process.stdout);
|
||||
docker.modem.demuxStream(stream, process.stdout, process.stderr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
rotate 7
|
||||
daily
|
||||
compress
|
||||
size=1M
|
||||
maxsize=1M
|
||||
missingok
|
||||
delaycompress
|
||||
copytruncate
|
||||
|
||||
@@ -15,64 +15,67 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<div class="layout-content">
|
||||
|
||||
<center>
|
||||
<br/>
|
||||
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>.</h4>
|
||||
<h2>Setup your account and password.</h2>
|
||||
</center>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
|
||||
<% if (user && user.username) { %>
|
||||
<div class="form-group"">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<div class="form-group"">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Display Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Display Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
+14
-13
@@ -2,25 +2,26 @@
|
||||
|
||||
<!-- error tester -->
|
||||
|
||||
<br/>
|
||||
<div class="layout-content">
|
||||
|
||||
<div class="container">
|
||||
<div class="container" style="margin-top: 50px;">
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger">
|
||||
<%- message %>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger">
|
||||
<%- message %>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="<%- adminOrigin %>">Back</a>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="<%- adminOrigin %>">Back</a>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,14 +27,15 @@
|
||||
|
||||
</head>
|
||||
|
||||
<body class="oauth">
|
||||
<body>
|
||||
|
||||
<div class="layout-root">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
|
||||
<a href="/" class="navbar-brand"><%= cloudronName %></a>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
|
||||
<a href="/" class="navbar-brand"><%= cloudronName %></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
+33
-37
@@ -2,45 +2,41 @@
|
||||
|
||||
<!-- login tester -->
|
||||
|
||||
<div class="container">
|
||||
<div class="layout-content">
|
||||
<div class="card" style="padding: 20px; margin-top: 50px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
|
||||
<h1><small>Login to</small> <%= applicationName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<% if (error) { %>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error"><%= error %></h4>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form id="loginForm" action="" method="post">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
|
||||
<h1><small>Login to</small> <%= applicationName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<% if (error) { -%>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error"><%= error %></h4>
|
||||
</div>
|
||||
</div>
|
||||
<% } -%>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form id="loginForm" action="" method="post">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -12,36 +12,41 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<h1>Hello <%= user.username %>, set a new password</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<center>
|
||||
<h2>Hello <%= user.username %>, set a new password</h2>
|
||||
</center>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Reset your password</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<br/>
|
||||
<center>
|
||||
<h2>Reset your password</h2>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<br/>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputIdentifier">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/login">Login</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputIdentifier">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/login">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Password reset successful</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<br/>
|
||||
<center>
|
||||
<h2>Password reset successful</h2>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<br/>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<center class="col-md-6 col-md-offset-3">
|
||||
<p>An email was sent to you with a link to set a new password.</p>
|
||||
<br/>
|
||||
<br/>
|
||||
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
|
||||
</center>
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<p>An email was sent to you with a link to set a new password.</p>
|
||||
<br/>
|
||||
<br/>
|
||||
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
+3
-1
@@ -329,7 +329,9 @@ function startMail(callback) {
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, callback);
|
||||
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+105
-3
@@ -17,8 +17,12 @@ exports = module.exports = {
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
exec: exec,
|
||||
execWebSocket: execWebSocket,
|
||||
|
||||
cloneApp: cloneApp
|
||||
cloneApp: cloneApp,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile
|
||||
};
|
||||
|
||||
var apps = require('../apps.js'),
|
||||
@@ -30,7 +34,8 @@ var apps = require('../apps.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
WebSocket = require('ws');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
@@ -58,7 +63,8 @@ function removeInternalAppFields(app) {
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
sso: app.sso,
|
||||
debugMode: app.debugMode,
|
||||
robotsTxt: app.robotsTxt
|
||||
robotsTxt: app.robotsTxt,
|
||||
enableBackup: app.enableBackup
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,6 +136,7 @@ function installApp(req, res, next) {
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
@@ -171,6 +178,8 @@ function configureApp(req, res, next) {
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
@@ -455,6 +464,58 @@ function exec(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function execWebSocket(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
cmd = safe.JSON.parse(req.query.cmd);
|
||||
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
|
||||
}
|
||||
|
||||
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
|
||||
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
|
||||
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
console.log('Connected to terminal');
|
||||
|
||||
req.clearTimeout();
|
||||
|
||||
res.handleUpgrade(function (ws) {
|
||||
duplexStream.on('end', function () { ws.close(); });
|
||||
duplexStream.on('close', function () { ws.close(); });
|
||||
duplexStream.on('error', function (error) {
|
||||
console.error('duplexStream error:', error);
|
||||
});
|
||||
duplexStream.on('data', function (data) {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(data.toString());
|
||||
});
|
||||
|
||||
ws.on('error', function (error) {
|
||||
console.error('websocket error:', error);
|
||||
});
|
||||
ws.on('message', function (msg) {
|
||||
duplexStream.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
// Clean things up, if any?
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
@@ -471,3 +532,44 @@ function listBackups(req, res, next) {
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
|
||||
|
||||
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
debug('uploadFile: done');
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('downloadFile: ', req.params.id, req.query.file);
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
|
||||
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': 'attachment; filename="' + info.filename + '"'
|
||||
};
|
||||
if (info.size) headers['Content-Length'] = info.size;
|
||||
|
||||
res.writeHead(200, headers);
|
||||
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,6 +184,10 @@ function getConfig(req, res, next) {
|
||||
cloudron.getConfig(function (error, cloudronConfig) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (!req.user.admin) {
|
||||
cloudronConfig = _.pick(cloudronConfig, 'apiServerOrigin', 'webServerOrigin', 'fqdn', 'version', 'progress', 'isCustomDomain', 'isDemo', 'cloudronName', 'provider');
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, cloudronConfig));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
var appdb = require('../appdb'),
|
||||
apps = require('../apps'),
|
||||
assert = require('assert'),
|
||||
auth = require('../auth.js'),
|
||||
authcodedb = require('../authcodedb'),
|
||||
clients = require('../clients'),
|
||||
ClientsError = clients.ClientsError,
|
||||
@@ -533,6 +534,25 @@ function scope(requestedScope) {
|
||||
];
|
||||
}
|
||||
|
||||
function websocketAuth(requestedScopes, req, res, next) {
|
||||
assert(Array.isArray(requestedScopes));
|
||||
|
||||
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
auth.accessTokenAuth(req.query.access_token, function (error, user, info) {
|
||||
if (error) return next(new HttpError(500, error.message));
|
||||
if (!user) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.user = user;
|
||||
req.authInfo = info;
|
||||
|
||||
var error = validateRequestedScopes(req, requestedScopes);
|
||||
if (error) return next(new HttpError(401, error.message));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Cross-site request forgery protection middleware for login form
|
||||
var csrf = [
|
||||
middleware.csrf(),
|
||||
@@ -559,5 +579,6 @@ exports = module.exports = {
|
||||
token: token,
|
||||
validateRequestedScopes: validateRequestedScopes,
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth,
|
||||
csrf: csrf
|
||||
};
|
||||
|
||||
@@ -34,14 +34,14 @@ var appdb = require('../../appdb.js'),
|
||||
taskmanager = require('../../taskmanager.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
url = require('url'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '24.0.1';
|
||||
var TEST_IMAGE_TAG = '25.2.0';
|
||||
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ var async = require('async'),
|
||||
database = require('../../database.js'),
|
||||
oauth2 = require('../oauth2.js'),
|
||||
expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
nock = require('nock'),
|
||||
hat = require('hat'),
|
||||
superagent = require('superagent'),
|
||||
|
||||
@@ -16,12 +16,14 @@ var async = require('async'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
shell = require('../../shell.js');
|
||||
shell = require('../../shell.js'),
|
||||
tokendb = require('../../tokendb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
@@ -204,6 +206,22 @@ describe('Cloudron', function () {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
token_1 = tokendb.generateToken();
|
||||
userId_1 = result.body.id;
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -239,7 +257,7 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
it('succeeds (admin)', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.get('/api/v1/boxes/localhost?token=' + config.token())
|
||||
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
|
||||
@@ -260,6 +278,7 @@ describe('Cloudron', function () {
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
expect(result.body.provider).to.be.a('string');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
@@ -267,6 +286,31 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds (non-admin)', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
console.dir(result.body);
|
||||
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
expect(result.body.provider).to.be.a('string');
|
||||
|
||||
expect(result.body.update).to.be(undefined);
|
||||
expect(result.body.size).to.be(undefined);
|
||||
expect(result.body.region).to.be(undefined);
|
||||
expect(result.body.memory).to.be(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
xdescribe('migrate', function () {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
async = require('async'),
|
||||
hat = require('hat'),
|
||||
urlParse = require('url').parse,
|
||||
|
||||
+22
-8
@@ -19,7 +19,8 @@ var assert = require('assert'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
path = require('path'),
|
||||
routes = require('./routes/index.js');
|
||||
routes = require('./routes/index.js'),
|
||||
ws = require('ws');
|
||||
|
||||
var gHttpServer = null;
|
||||
var gSysadminHttpServer = null;
|
||||
@@ -28,6 +29,8 @@ function initializeExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
const wsServer = new ws.Server({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade
|
||||
|
||||
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
@@ -78,7 +81,7 @@ function initializeExpressSync() {
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// NOTE: these limits have to be in sync with nginx limits
|
||||
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
|
||||
var FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
|
||||
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
|
||||
|
||||
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
|
||||
@@ -189,7 +192,11 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
|
||||
// websocket cannot do bearer authentication
|
||||
router.get ('/api/v1/apps/:id/execws', routes.oauth2.websocketAuth.bind(null, [ clients.SCOPE_APPS ]), routes.user.requireAdmin, routes.apps.execWebSocket);
|
||||
router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp);
|
||||
router.get ('/api/v1/apps/:id/download', appsScope, routes.user.requireAdmin, routes.apps.downloadFile);
|
||||
router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile);
|
||||
|
||||
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
|
||||
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
|
||||
@@ -237,12 +244,19 @@ function initializeExpressSync() {
|
||||
// create a node response object for express
|
||||
var res = new http.ServerResponse({});
|
||||
res.assignSocket(socket);
|
||||
res.sendUpgradeHandshake = function () { // could extend express.response as well
|
||||
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
|
||||
'Upgrade: tcp\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'\r\n');
|
||||
};
|
||||
|
||||
if (req.headers.upgrade === 'websocket') {
|
||||
res.handleUpgrade = function (callback) {
|
||||
wsServer.handleUpgrade(req, socket, head, callback);
|
||||
};
|
||||
} else {
|
||||
res.sendUpgradeHandshake = function () { // could extend express.response as well
|
||||
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
|
||||
'Upgrade: tcp\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'\r\n');
|
||||
};
|
||||
}
|
||||
|
||||
// route through express middleware. if we provide no callback, express will provide a 'finalhandler'
|
||||
// TODO: it's not clear if socket needs to be destroyed
|
||||
|
||||
@@ -40,7 +40,6 @@ SubdomainError.NOT_FOUND = 'No such domain';
|
||||
SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.BAD_FIELD = 'Bad Field';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
|
||||
SubdomainError.INTERNAL_ERROR = 'Internal error';
|
||||
SubdomainError.ACCESS_DENIED = 'Access denied';
|
||||
SubdomainError.INVALID_PROVIDER = 'provider must be route53, digitalocean, cloudflare, noop, manual or caas';
|
||||
|
||||
@@ -30,7 +30,7 @@ var MANIFEST = {
|
||||
"contactEmail": "support@cloudron.io",
|
||||
"version": "0.1.0",
|
||||
"manifestVersion": 1,
|
||||
"dockerImage": "cloudron/test:24.0.1",
|
||||
"dockerImage": "cloudron/test:25.2.0",
|
||||
"healthCheckPath": "/",
|
||||
"httpPort": 7777,
|
||||
"tcpPorts": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
readonly TEST_IMAGE="cloudron/test:24.0.1"
|
||||
readonly TEST_IMAGE="cloudron/test:25.2.0"
|
||||
|
||||
# reset sudo timestamp to avoid wrong success
|
||||
sudo -k || sudo --reset-timestamp
|
||||
|
||||
@@ -543,7 +543,8 @@ describe('database', function () {
|
||||
xFrameOptions: 'DENY',
|
||||
sso: true,
|
||||
debugMode: null,
|
||||
robotsTxt: null
|
||||
robotsTxt: null,
|
||||
enableBackup: true
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
@@ -566,7 +567,8 @@ describe('database', function () {
|
||||
xFrameOptions: 'SAMEORIGIN',
|
||||
sso: true,
|
||||
debugMode: null,
|
||||
robotsTxt: null
|
||||
robotsTxt: null,
|
||||
enableBackup: true
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
|
||||
rm -rf $HOME/.cloudron_test 2>/dev/null || true # some of those docker container data requires sudo to be removed
|
||||
mkdir -p $HOME/.cloudron_test
|
||||
cd $HOME/.cloudron_test
|
||||
mkdir -p appsdata boxdata/appicons platformdata/mail platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons configs boxdata/certs platformdata/mail/dkim/localhost platformdata/mail/dkim/foobar.com
|
||||
mkdir -p appsdata boxdata/appicons platformdata/mail platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons configs boxdata/certs platformdata/mail/dkim/localhost platformdata/mail/dkim/foobar.com platformdata/logrotate.d/
|
||||
|
||||
# put cert
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ var assert = require('assert'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
* @module xterm/addons/attach/attach
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (attach) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = attach(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], attach);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
attach(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
exports.attach = function (term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
term._flushBuffer = function () {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
clearTimeout(term._attachSocketBufferTimer);
|
||||
term._attachSocketBufferTimer = null;
|
||||
};
|
||||
|
||||
term._pushToBuffer = function (data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
term._getMessage = function (ev) {
|
||||
if (buffered) {
|
||||
term._pushToBuffer(ev.data);
|
||||
} else {
|
||||
term.write(ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
term._sendData = function (data) {
|
||||
socket.send(data);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', term.detach.bind(term, socket));
|
||||
socket.addEventListener('error', term.detach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
exports.detach = function (term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', term._getMessage);
|
||||
}
|
||||
|
||||
delete term.socket;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the current terminal to the given socket
|
||||
*
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
Xterm.prototype.attach = function (socket, bidirectional, buffered) {
|
||||
return exports.attach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the current terminal from the given socket.
|
||||
*
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
Xterm.prototype.detach = function (socket) {
|
||||
return exports.detach(this, socket);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Fit terminal columns and rows to the dimensions of its DOM element.
|
||||
*
|
||||
* ## Approach
|
||||
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
|
||||
*
|
||||
* - Columns: Truncate the division of the terminal parent element width by the terminal character
|
||||
* width (apply display: inline at the terminal row and truncate its width with the current
|
||||
* number of columns).
|
||||
* @module xterm/addons/fit/fit
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (fit) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fit(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fit);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fit(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
exports.proposeGeometry = function (term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||
elementStyle = window.getComputedStyle(term.element),
|
||||
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
|
||||
availableHeight = parentElementHeight - elementPaddingVer,
|
||||
availableWidth = parentElementWidth - elementPaddingHor,
|
||||
container = term.rowContainer,
|
||||
subjectRow = term.rowContainer.firstElementChild,
|
||||
contentBuffer = subjectRow.innerHTML,
|
||||
characterHeight,
|
||||
rows,
|
||||
characterWidth,
|
||||
cols,
|
||||
geometry;
|
||||
|
||||
subjectRow.style.display = 'inline';
|
||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||
subjectRow.innerHTML = contentBuffer;
|
||||
|
||||
rows = parseInt(availableHeight / characterHeight);
|
||||
cols = parseInt(availableWidth / characterWidth);
|
||||
|
||||
geometry = {cols: cols, rows: rows};
|
||||
return geometry;
|
||||
};
|
||||
|
||||
exports.fit = function (term) {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
if (geometry) {
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
};
|
||||
|
||||
Xterm.prototype.proposeGeometry = function () {
|
||||
return exports.proposeGeometry(this);
|
||||
};
|
||||
|
||||
Xterm.prototype.fit = function () {
|
||||
return exports.fit(this);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
.xterm.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 255;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Fullscreen addon for xterm.js
|
||||
* @module xterm/addons/fullscreen/fullscreen
|
||||
* @license MIT
|
||||
*/
|
||||
(function (fullscreen) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fullscreen(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fullscreen);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fullscreen(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Toggle the given terminal's fullscreen mode.
|
||||
* @param {Xterm} term - The terminal to toggle full screen mode
|
||||
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||
*/
|
||||
exports.toggleFullScreen = function (term, fullscreen) {
|
||||
var fn;
|
||||
|
||||
if (typeof fullscreen == 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
} else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
} else {
|
||||
fn = 'add';
|
||||
}
|
||||
|
||||
term.element.classList[fn]('fullscreen');
|
||||
};
|
||||
|
||||
Xterm.prototype.toggleFullscreen = function (fullscreen) {
|
||||
exports.toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
||||
* @module xterm/addons/linkify/linkify
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (linkify) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = linkify(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], linkify);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
linkify(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {},
|
||||
protocolClause = '(https?:\\/\\/)',
|
||||
domainCharacterSet = '[\\da-z\\.-]+',
|
||||
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
||||
domainBodyClause = '(' + domainCharacterSet + ')',
|
||||
tldClause = '([a-z\\.]{2,6})',
|
||||
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
||||
portClause = '(:\\d{1,5})',
|
||||
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
||||
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
||||
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
||||
bodyClause = hostClause + pathClause,
|
||||
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
||||
end = ')($|' + negatedPathCharacterSet + ')',
|
||||
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
||||
strictUrlClause = start + protocolClause + bodyClause + end,
|
||||
lenientUrlRegex = new RegExp(lenientUrlClause),
|
||||
strictUrlRegex = new RegExp(strictUrlClause);
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the given terminal line into
|
||||
* hyperlinks. The terminal line can be either the HTML element itself
|
||||
* or the index of the termina line in the children of the terminal
|
||||
* rows container.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
||||
if (typeof line == 'number') {
|
||||
line = terminal.rowContainer.children[line];
|
||||
} else if (! (line instanceof HTMLDivElement)) {
|
||||
var message = 'The "line" argument should be either a number';
|
||||
message += ' or an HTMLDivElement';
|
||||
|
||||
throw new TypeError(message);
|
||||
}
|
||||
|
||||
if (typeof target === 'undefined') {
|
||||
target = '';
|
||||
} else {
|
||||
target = 'target="' + target + '"';
|
||||
}
|
||||
|
||||
var buffer = document.createElement('span'),
|
||||
nodes = line.childNodes;
|
||||
|
||||
for (var j=0; j<nodes.length; j++) {
|
||||
var node = nodes[j],
|
||||
match;
|
||||
|
||||
/**
|
||||
* Since we cannot access the TextNode's HTML representation
|
||||
* from the instance itself, we assign its data as textContent
|
||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
||||
* HTML representation from the buffer's innerHTML.
|
||||
*/
|
||||
buffer.textContent = node.data;
|
||||
|
||||
var nodeHTML = buffer.innerHTML;
|
||||
|
||||
/**
|
||||
* Apply function only on TextNodes
|
||||
*/
|
||||
if (node.nodeType != node.TEXT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = exports.findLinkMatch(node.data, lenient);
|
||||
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
||||
urlHasProtocol = url.match(startsWithProtocol),
|
||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
||||
newHTML = nodeHTML.replace(url, link);
|
||||
|
||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL susbtrings
|
||||
* to HTML anchor elements (links) has finished, for a specific
|
||||
* line of the current Xterm instance.
|
||||
*
|
||||
* @event linkify:line
|
||||
*/
|
||||
terminal.emit('linkify:line', line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a link within a block of text.
|
||||
*
|
||||
* @param {string} text - The text to search .
|
||||
* @param {boolean} lenient - Whether to use the lenient search.
|
||||
* @return {string} A URL.
|
||||
*/
|
||||
exports.findLinkMatch = function (text, lenient) {
|
||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
||||
if (!match || match.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkify = function (terminal, lenient, target) {
|
||||
var rows = terminal.rowContainer.children;
|
||||
|
||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var line = rows[i];
|
||||
|
||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL substrings to
|
||||
* HTML anchor elements (links) has finished for the current Xterm
|
||||
* instance's view.
|
||||
*
|
||||
* @event linkify
|
||||
*/
|
||||
terminal.emit('linkify');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend Xterm prototype.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal linte into
|
||||
* hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkify = function (lenient, target) {
|
||||
return exports.linkify(this, lenient, target);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* This module provides methods for attaching a terminal to a terminado WebSocket stream.
|
||||
*
|
||||
* @module xterm/addons/terminado/terminado
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (attach) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = attach(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], attach);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
attach(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
term._flushBuffer = function () {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
clearTimeout(term._attachSocketBufferTimer);
|
||||
term._attachSocketBufferTimer = null;
|
||||
};
|
||||
|
||||
term._pushToBuffer = function (data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
term._getMessage = function (ev) {
|
||||
var data = JSON.parse(ev.data)
|
||||
if( data[0] == "stdout" ) {
|
||||
if (buffered) {
|
||||
term._pushToBuffer(data[1]);
|
||||
} else {
|
||||
term.write(data[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
term._sendData = function (data) {
|
||||
socket.send(JSON.stringify(['stdin', data]));
|
||||
};
|
||||
|
||||
term._setSize = function (size) {
|
||||
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||
};
|
||||
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
term.on('resize', term._setSize);
|
||||
|
||||
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
||||
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
exports.terminadoDetach = function (term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', term._getMessage);
|
||||
}
|
||||
|
||||
delete term.socket;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the current terminal to the given socket
|
||||
*
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
Xterm.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
||||
return exports.terminadoAttach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the current terminal from the given socket.
|
||||
*
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
Xterm.prototype.terminadoDetach = function (socket) {
|
||||
return exports.terminadoDetach(this, socket);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
+2261
File diff suppressed because it is too large
Load Diff
Vendored
+5132
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src * data:;" />
|
||||
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src * data:;" /> -->
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
@@ -72,6 +72,12 @@
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="3rdparty/js/moment.min.js"></script>
|
||||
|
||||
<!-- xterm -->
|
||||
<link href="3rdparty/xterm/xterm.css" rel="stylesheet">
|
||||
<script src="3rdparty/xterm/xterm.js"></script>
|
||||
<script src="3rdparty/xterm/addons/attach/attach.js"></script>
|
||||
<script src="3rdparty/xterm/addons/fit/fit.js"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script src="js/index.js"></script>
|
||||
|
||||
@@ -219,9 +225,9 @@
|
||||
<li ng-show="user.admin"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> Domain & Certs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
|
||||
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/logs"><i class="fa fa-file-text fa-fw"></i> Logs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
<li ng-show="user.admin" class="divider"></li>
|
||||
<li ng-show="user.admin"><a href="#/debug"><i class="fa fa-terminal fa-fw"></i> Terminal & Logs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
|
||||
|
||||
@@ -68,6 +68,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
return $http.get(client.apiOrigin + url, config);
|
||||
}
|
||||
|
||||
function head(url, config) {
|
||||
config = config || {};
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = 'Bearer ' + token;
|
||||
|
||||
return $http.head(client.apiOrigin + url, config);
|
||||
}
|
||||
|
||||
function post(url, data, config) {
|
||||
data = data || {};
|
||||
config = config || {};
|
||||
@@ -120,7 +128,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._installedApps = [];
|
||||
this._clientId = '<%= oauth.clientId %>';
|
||||
this._clientSecret = '<%= oauth.clientSecret %>';
|
||||
this.apiOrigin = '<%= oauth.apiOrigin %>';
|
||||
// window.location fallback for websocket connections which do not have relative uris
|
||||
this.apiOrigin = '<%= oauth.apiOrigin %>' || window.location.origin;
|
||||
this.avatar = '';
|
||||
|
||||
this.resetAvatar();
|
||||
@@ -241,6 +250,10 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
token = accessToken;
|
||||
};
|
||||
|
||||
Client.prototype.getToken = function () {
|
||||
return token;
|
||||
};
|
||||
|
||||
/*
|
||||
* Rest API wrappers
|
||||
*/
|
||||
@@ -338,7 +351,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
memoryLimit: config.memoryLimit,
|
||||
altDomain: config.altDomain || null,
|
||||
xFrameOptions: config.xFrameOptions,
|
||||
robotsTxt: config.robotsTxt || null
|
||||
robotsTxt: config.robotsTxt || null,
|
||||
enableBackup: config.enableBackup
|
||||
};
|
||||
|
||||
post('/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
|
||||
@@ -1095,6 +1109,31 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
return (available - needed) >= 0;
|
||||
};
|
||||
|
||||
Client.prototype.uploadFile = function (appId, file, progressCallback, callback) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
post('/api/v1/apps/' + appId + '/upload?file=/tmp/' + file.name, fd, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
transformRequest: angular.identity,
|
||||
uploadEventHandlers: {
|
||||
progress: progressCallback
|
||||
}
|
||||
}).success(function(data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.checkDownloadableFile = function (appId, filePath, callback) {
|
||||
head('/api/v1/apps/' + appId + '/download?file=' + filePath, {
|
||||
headers: { 'Content-Type': undefined }
|
||||
}).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
client = new Client();
|
||||
return client;
|
||||
}]);
|
||||
|
||||
@@ -43,9 +43,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/graphs', {
|
||||
controller: 'GraphsController',
|
||||
templateUrl: 'views/graphs.html'
|
||||
}).when('/logs', {
|
||||
controller: 'LogsController',
|
||||
templateUrl: 'views/logs.html'
|
||||
}).when('/debug', {
|
||||
controller: 'DebugController',
|
||||
templateUrl: 'views/debug.html'
|
||||
}).when('/certs', {
|
||||
controller: 'CertsController',
|
||||
templateUrl: 'views/certs.html'
|
||||
|
||||
+21
-10
@@ -140,22 +140,33 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
|
||||
if (result.provider === 'caas') return;
|
||||
|
||||
Client.getMailRelay(function (error, result) {
|
||||
Client.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// the email status checks are currently only useful when using Cloudron itself for relaying
|
||||
if (result.provider !== 'cloudron-smtp') return;
|
||||
if (backupConfig.provider === 'noop') {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/settings';
|
||||
|
||||
// Check if all email DNS records are set up properly only for non external DNS API
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
Client.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope);
|
||||
}
|
||||
|
||||
Client.getMailRelay(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/email';
|
||||
// the email status checks are currently only useful when using Cloudron itself for relaying
|
||||
if (result.provider !== 'cloudron-smtp') return;
|
||||
|
||||
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
|
||||
}
|
||||
// Check if all email DNS records are set up properly only for non external DNS API
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/email';
|
||||
|
||||
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+63
-51
@@ -121,10 +121,15 @@ html, body {
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex-grow: 2;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#ng-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -137,14 +142,24 @@ html, body {
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 970px;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media(min-width:768px) {
|
||||
width: 720px;
|
||||
|
||||
&.content-large {
|
||||
width: 970px;
|
||||
max-width: 970px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
.navbar-collapse {
|
||||
background-color: #F8F8F8;
|
||||
@@ -454,11 +469,6 @@ h1, h2, h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
max-width: 720px;
|
||||
@@ -508,7 +518,7 @@ h1, h2, h3 {
|
||||
|
||||
.grid-item-top .progress {
|
||||
border-radius: 0;
|
||||
box-shadown: none;
|
||||
box-shadow: none;
|
||||
margin-top: 10px;
|
||||
width: inherit;
|
||||
height: 10px;
|
||||
@@ -516,7 +526,7 @@ h1, h2, h3 {
|
||||
|
||||
.grid-item-top .progress-bar {
|
||||
border-radius: 0;
|
||||
box-shadown: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
@@ -680,7 +690,7 @@ h1, h2, h3 {
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-grow: 1;
|
||||
flex-grow: 0;
|
||||
background-color: #f8f8f8;
|
||||
width: 100%;
|
||||
color: #555;
|
||||
@@ -823,29 +833,6 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Oauth classes
|
||||
// ----------------------------
|
||||
|
||||
.oauth {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: #F7F7F7;
|
||||
|
||||
h1 {
|
||||
font-size: 33px;
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: none;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// Graphs classes
|
||||
// ----------------------------
|
||||
@@ -1106,26 +1093,42 @@ footer {
|
||||
// Logs
|
||||
// ----------------------------
|
||||
|
||||
.logs-main {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.logs-controls {
|
||||
margin-top: 25px;
|
||||
|
||||
.log-line-container {
|
||||
flex-grow: 1;
|
||||
background-color: black;
|
||||
color: white;
|
||||
overflow: auto;
|
||||
border-style: solid;
|
||||
border-width: 0 15px;
|
||||
border-color: $body-bg;
|
||||
padding: 5px;
|
||||
font-family: monospace;
|
||||
margin-bottom: 20px;
|
||||
.ng-isolate-scope {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
select {
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.uib-tab.active {
|
||||
a {
|
||||
background-color: white;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-and-term-container {
|
||||
flex-grow: 1;
|
||||
margin-left: calc(8.33% + 15px);
|
||||
margin-right: calc(8.33% + 15px);
|
||||
margin-bottom: 20px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
font-family: monospace;
|
||||
|
||||
.log-line {
|
||||
line-height: 1.2;
|
||||
|
||||
@@ -1138,3 +1141,12 @@ footer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextMenuBackdrop {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
+46
-41
@@ -1,62 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' *.cloudron.io <%= apiOriginHostname %>" />
|
||||
|
||||
<title> Cloudron </title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' *.cloudron.io <%= apiOriginHostname %>" />
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
<title> Cloudron </title>
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
<!-- Custom Fonts -->
|
||||
<link href="3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="3rdparty/js/bootstrap.min.js"></script>
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script src="3rdparty/js/angular.min.js"></script>
|
||||
<script src="3rdparty/js/angular-loader.min.js"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Update Application -->
|
||||
<script src="js/update.js"></script>
|
||||
<!-- Angularjs scripts -->
|
||||
<script src="3rdparty/js/angular.min.js"></script>
|
||||
<script src="3rdparty/js/angular-loader.min.js"></script>
|
||||
|
||||
<!-- Update Application -->
|
||||
<script src="js/update.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F">
|
||||
<div class="modal show" id="updateProgressModal" tabindex="-1" role="dialog" aria-labelledby="updateProgressModalLabel" aria-hidden="true" data-keyboard="false" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{title}}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="!error">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{percent}}%"></div>
|
||||
</div>
|
||||
<span>{{message}}</span>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="error">
|
||||
<span>{{message}}</span>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="error">
|
||||
<button type="button" class="btn btn-primary" ng-click="loadWebadmin()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal show" id="updateProgressModal" tabindex="-1" role="dialog" aria-labelledby="updateProgressModalLabel" aria-hidden="true" data-keyboard="false" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{title}}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="!error">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{percent}}%"></div>
|
||||
</div>
|
||||
<span>{{message}}</span>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="error">
|
||||
<span>{{message}}</span>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="error">
|
||||
<button type="button" class="btn btn-primary" ng-click="loadWebadmin()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-root">
|
||||
<div class="layout-content"></div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
<span class="text-muted">©2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -101,69 +101,68 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Account</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<img width="128" height="128" ng-src="{{ user.gravatarHuge }}"/>
|
||||
</div>
|
||||
<div class="col-xs-8 text-medium">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Username</td>
|
||||
<td class="text-right" style="vertical-align: top;">{{ user.username }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Display name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.displayName }} <a href="" ng-click="displayNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.email }} <a href="" ng-click="emailchange.show()" ng-show="!user.alternateEmail"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr ng-show="user.alternateEmail">
|
||||
<td class="text-muted" style="vertical-align: top;">Password recovery email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.alternateEmail }} <a href="" ng-click="emailchange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="passwordchange.show()">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h1>Account</h1>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<img width="128" height="128" ng-src="{{ user.gravatarHuge }}"/>
|
||||
</div>
|
||||
<div class="col-xs-8 text-medium">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Username</td>
|
||||
<td class="text-right" style="vertical-align: top;">{{ user.username }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Display name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.displayName }} <a href="" ng-click="displayNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.email }} <a href="" ng-click="emailchange.show()" ng-show="!user.alternateEmail"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr ng-show="user.alternateEmail">
|
||||
<td class="text-muted" style="vertical-align: top;">Password recovery email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.alternateEmail }} <a href="" ng-click="emailchange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="passwordchange.show()">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Sessions</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>You are logged into {{ activeClients.length + 1 }} app(s), including this session.</p>
|
||||
<span ng-show="activeTokenCount > 1">
|
||||
<hr/>
|
||||
<h4>Active Apps:</h4>
|
||||
<p ng-repeat="client in activeClients"><b>{{ client.name }} - {{client.activeTokens.length}} time(s)</b></p>
|
||||
<hr/>
|
||||
</span>
|
||||
<button class="btn btn-outline btn-xs btn-danger pull-right" ng-click="revokeTokens()">Logout From All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3>Sessions</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>You are logged into {{ activeClients.length + 1 }} app(s), including this session.</p>
|
||||
<span ng-show="activeTokenCount > 1">
|
||||
<hr/>
|
||||
<h4>Active Apps:</h4>
|
||||
<p ng-repeat="client in activeClients"><b>{{ client.name }} - {{client.activeTokens.length}} time(s)</b></p>
|
||||
<hr/>
|
||||
</span>
|
||||
<button class="btn btn-outline btn-xs btn-danger pull-right" ng-click="revokeTokens()">Logout From All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,6 +133,11 @@
|
||||
<textarea ng-model="appConfigure.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="appConfigureEnableBackup" ng-model="appConfigure.enableBackup">
|
||||
<label class="control-label" for="appConfigureEnableBackup">Enable backups</label>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
||||
@@ -172,7 +177,6 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="restartApp(appConfigure.app)" ng-disabled="restartAppBusy"><i class="fa fa-circle-o-notch fa-spin" ng-show="restartAppBusy"></i> Restart</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
@@ -356,7 +360,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.admin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
@@ -410,7 +417,7 @@
|
||||
<div class="grid-item-bottom-mobile" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-left">
|
||||
<a href="" ng-click="showRestore(app)">
|
||||
<a href="" ng-click="showRestore(app)" ng-show="backupConfig.provider !== 'noop'">
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
@@ -433,7 +440,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
<a href="" ng-click="showRestore(app)" ng-show="backupConfig.provider !== 'noop'" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
|
||||
+14
-37
@@ -10,8 +10,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.dnsConfig = {};
|
||||
$scope.groups = [];
|
||||
$scope.users = [];
|
||||
$scope.restartAppBusy = false;
|
||||
$scope.mailConfig = {};
|
||||
$scope.backupConfig = {};
|
||||
|
||||
$scope.appConfigure = {
|
||||
busy: false,
|
||||
@@ -122,6 +122,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.xFrameOptions = '';
|
||||
$scope.appConfigure.customAuth = false;
|
||||
$scope.appConfigure.robotsTxt = '';
|
||||
$scope.appConfigure.enableBackup = true;
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
@@ -215,6 +216,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// create ticks starting from manifest memory limit
|
||||
$scope.appConfigure.memoryTicks = [
|
||||
@@ -268,7 +270,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
|
||||
robotsTxt: $scope.appConfigure.robotsTxt
|
||||
robotsTxt: $scope.appConfigure.robotsTxt,
|
||||
enableBackup: $scope.appConfigure.enableBackup
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, data, function (error) {
|
||||
@@ -532,41 +535,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
return app.manifest && app.manifest.configurePath;
|
||||
};
|
||||
|
||||
$scope.restartApp = function (app) {
|
||||
$scope.restartAppBusy = true;
|
||||
|
||||
function waitUntilStopped(callback) {
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
Client.getApp(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.runState === 'stopped') return callback();
|
||||
setTimeout(waitUntilStopped.bind(null, callback), 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.stopApp(app.id, function (error) {
|
||||
$scope.restartAppBusy = false;
|
||||
|
||||
if (error) return console.error('Failed to stop app.', error);
|
||||
|
||||
// close dialog to allow user see the app restarting
|
||||
$('#appConfigureModal').modal('hide');
|
||||
$scope.reset();
|
||||
|
||||
waitUntilStopped(function (error) {
|
||||
if (error) return console.error('Failed to get app status.', error);
|
||||
|
||||
Client.startApp(app.id, function (error) {
|
||||
if (error) console.error('Failed to start app.', error);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getUsers(function (error, users) {
|
||||
if (error) {
|
||||
@@ -608,6 +576,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig() {
|
||||
Client.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backupConfig = backupConfig;
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.refreshUserInfo(function (error) {
|
||||
if (error) return console.error(error);
|
||||
@@ -617,6 +593,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
fetchGroups();
|
||||
fetchDnsConfig();
|
||||
getMailConfig();
|
||||
getBackupConfig();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+155
-156
@@ -45,6 +45,9 @@
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" id="dnsCredentialsCloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- this will be autofilled by most browsers regardless of the attribute, since the next field is a password field.... -->
|
||||
<input type="text" class="form-control hide">
|
||||
|
||||
<!-- all provider -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
@@ -87,160 +90,156 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Domain & Certificates</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Domain</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!config.isCustomDomain">To use a custom domain, configure your domain to use <a target="_blank" href="https://aws.amazon.com/route53/">Route53.</a> Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Domain name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.fqdn }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">DNS provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.provider }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && !dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider is configured. All DNS records need to be setup manually.
|
||||
To avoid manual setup for each installed app, set a DNS API provider.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'noop'">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Access key id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId || 'unset' }}</td>
|
||||
</tr>
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Secret access key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.secretAccessKey"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'digitalocean'">
|
||||
<td class="text-muted" style="vertical-align: top;">DigitalOcean token</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.token"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<!-- add some space -->
|
||||
<tr>
|
||||
<td><br/></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><button class="btn btn-outline btn-primary" ng-click="showChangeDnsCredentials()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>SSL Certificates</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="!config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
Certificates can only by set for custom domains.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<p>Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.</p>
|
||||
<br/>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="defaultCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row hide">
|
||||
<div class="col-md-12">
|
||||
<form name="adminCertForm" ng-submit="setAdminCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="adminCertInput">Settings Certificate</label>
|
||||
<p>This certificate will be used for this Settings application.</p>
|
||||
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="adminCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domain & Certificates</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Domain</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!config.isCustomDomain">To use a custom domain, configure your domain to use <a target="_blank" href="https://aws.amazon.com/route53/">Route53.</a> Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Domain name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.fqdn }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">DNS provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.provider }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && !dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider is configured. All DNS records need to be setup manually.
|
||||
To avoid manual setup for each installed app, set a DNS API provider.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'noop'">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Access key id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId || 'unset' }}</td>
|
||||
</tr>
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Secret access key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.secretAccessKey"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'digitalocean'">
|
||||
<td class="text-muted" style="vertical-align: top;">DigitalOcean token</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.token"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<!-- add some space -->
|
||||
<tr>
|
||||
<td><br/></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><button class="btn btn-outline btn-primary" ng-click="showChangeDnsCredentials()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>SSL Certificates</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="!config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
Certificates can only by set for custom domains.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<p>Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.</p>
|
||||
<br/>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="defaultCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row hide">
|
||||
<div class="col-md-12">
|
||||
<form name="adminCertForm" ng-submit="setAdminCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="adminCertInput">Settings Certificate</label>
|
||||
<p>This certificate will be used for this Settings application.</p>
|
||||
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="adminCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- Modal download file -->
|
||||
<div class="modal fade" id="downloadFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Download from {{ selected.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="downloadFileForm" ng-submit="downloadFile.submit()">
|
||||
<div class="form-group" ng-class="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<label class="control-label" for="inputDownloadFilePath">Path to file or directory</label>
|
||||
<div class="control-label" ng-show="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<small>{{ downloadFile.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="filePath" ng-model="downloadFile.filePath" required autofocus>
|
||||
</div>
|
||||
<input id="inputDownloadFilePath" class="ng-hide" type="submit" ng-disabled="!downloadFile.filePath"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-o-notch fa-spin" ng-show="downloadFile.busy"></i> Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload progress -->
|
||||
<div class="modal fade" id="uploadProgressModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Uploading file to {{ selected.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span><b>{{ (uploadProgress.current/1000/1000).toFixed(2) }}mb</b> (total {{ (uploadProgress.total/1000/1000).toFixed(2) }}mb)</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ 100*(uploadProgress.current/uploadProgress.total) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-controls">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<uib-tabset active="active">
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
<uib-tab index="0" heading="Logs" select="showLogs()"></uib-tab>
|
||||
</uib-tabset>
|
||||
<select class="form-control pull-right inline" ng-options="log.name for log in logs track by log.value" ng-model="selected"></select>
|
||||
|
||||
<!-- logs actions -->
|
||||
<a class="btn btn-default pull-right" ng-href="{{ selected.url }}&format=short&lines=800" ng-hide="terminalVisible"><i class="fa fa-download"></i> Download Logs</a>
|
||||
|
||||
<input type="file" id="fileUpload" class="hide"/>
|
||||
|
||||
<!-- terminal actions -->
|
||||
<div class="btn-group pull-right" style="margin-left: 10px;">
|
||||
<button class="btn btn-default" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="restartAppBusy"><i class="fa fa-circle-o-notch fa-spin" ng-show="restartAppBusy"></i> Restart</button>
|
||||
<button class="btn btn-default" ng-click="uploadFile()" ng-show="terminalVisible && selected.type === 'app' && !uploadProgress.busy"><i class="fa fa-upload"></i> Upload to /tmp</button>
|
||||
<button class="btn btn-default" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy"><i class="fa fa-circle-o-notch fa-spin"></i> Uploading...</button>
|
||||
<button class="btn btn-default" ng-click="downloadFile.show()" ng-show="terminalVisible && selected.type === 'app'"><i class="fa fa-download"></i> Download</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group pull-right" style="margin-left: 10px;">
|
||||
<button class="btn btn-default" ng-click="terminalInject('mysql')" ng-show="terminalVisible && usesAddon('mysql')">MySQL</button>
|
||||
<button class="btn btn-default" ng-click="terminalInject('postgresql')" ng-show="terminalVisible && usesAddon('postgresql')">Postgres</button>
|
||||
<button class="btn btn-default" ng-click="terminalInject('mongodb')" ng-show="terminalVisible && usesAddon('mongodb')">MongoDB</button>
|
||||
<button class="btn btn-default" ng-click="terminalInject('redis')" ng-show="terminalVisible && usesAddon('redis')">Redis</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-and-term-container"></div>
|
||||
|
||||
<div class="contextMenuBackdrop">
|
||||
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
|
||||
<li><a href="" ng-click="terminalCopy()">Copy</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="" ng-click="terminalClear()">Clear</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,339 @@
|
||||
'use strict';
|
||||
|
||||
/* global moment */
|
||||
/* global Terminal */
|
||||
|
||||
|
||||
angular.module('Application').controller('DebugController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.terminalVisible = true;
|
||||
$scope.logs = [];
|
||||
$scope.selected = '';
|
||||
$scope.activeEventSource = null;
|
||||
$scope.terminal = null;
|
||||
$scope.terminalSocket = null;
|
||||
$scope.lines = 10;
|
||||
$scope.restartAppBusy = false;
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
$scope.downloadFile = {
|
||||
error: '',
|
||||
filePath: '',
|
||||
busy: false,
|
||||
|
||||
downloadUrl: function () {
|
||||
if (!$scope.downloadFile.filePath) return '';
|
||||
|
||||
var filePath = $scope.downloadFile.filePath.replace(/\/*\//g, '/');
|
||||
|
||||
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.downloadFile.busy = false;
|
||||
$scope.downloadFile.error = '';
|
||||
$scope.downloadFile.filePath = '';
|
||||
$('#downloadFileModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.downloadFile.busy = true;
|
||||
|
||||
Client.checkDownloadableFile($scope.selected.value, $scope.downloadFile.filePath, function (error) {
|
||||
$scope.downloadFile.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.downloadFile.error = 'The requested file does not exist.';
|
||||
return;
|
||||
}
|
||||
|
||||
// we have to click the link to make the browser do the download
|
||||
// don't know how to prevent the browsers
|
||||
$('#fileDownloadLink')[0].click();
|
||||
|
||||
$('#downloadFileModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uploadProgress = {
|
||||
busy: false,
|
||||
total: 0,
|
||||
current: 0,
|
||||
|
||||
show: function () {
|
||||
$scope.uploadProgress.total = 0;
|
||||
$scope.uploadProgress.current = 0;
|
||||
|
||||
$('#uploadProgressModal').modal('show');
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
$('#uploadProgressModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uploadFile = function () {
|
||||
var fileUpload = document.querySelector('#fileUpload');
|
||||
|
||||
fileUpload.oninput = function (e) {
|
||||
$scope.uploadProgress.busy = true;
|
||||
$scope.uploadProgress.show();
|
||||
|
||||
Client.uploadFile($scope.selected.value, e.target.files[0], function progress(e) {
|
||||
$scope.uploadProgress.total = e.total;
|
||||
$scope.uploadProgress.current = e.loaded;
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.uploadProgress.busy = false;
|
||||
$scope.uploadProgress.hide();
|
||||
});
|
||||
};
|
||||
|
||||
fileUpload.click();
|
||||
};
|
||||
|
||||
$scope.populateLogTypes = function () {
|
||||
$scope.logs.push({ name: 'System (All)', type: 'platform', value: 'all', url: Client.makeURL('/api/v1/cloudron/logs?units=all') });
|
||||
$scope.logs.push({ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs?units=box') });
|
||||
$scope.logs.push({ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs?units=mail') });
|
||||
|
||||
Client.getInstalledApps().forEach(function (app) {
|
||||
$scope.logs.push({
|
||||
type: 'app',
|
||||
value: app.id,
|
||||
name: app.fqdn + ' (' + app.manifest.title + ')',
|
||||
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
|
||||
addons: app.manifest.addons
|
||||
});
|
||||
});
|
||||
|
||||
$scope.selected = $scope.logs[0];
|
||||
};
|
||||
|
||||
$scope.usesAddon = function (addon) {
|
||||
if (!$scope.selected || !$scope.selected.addons) return false;
|
||||
return !!Object.keys($scope.selected.addons).find(function (a) { return a === addon; });
|
||||
};
|
||||
|
||||
function reset() {
|
||||
// close the old event source so we wont receive any new logs
|
||||
if ($scope.activeEventSource) {
|
||||
$scope.activeEventSource.close();
|
||||
$scope.activeEventSource = null;
|
||||
}
|
||||
|
||||
var logViewer = $('.logs-and-term-container');
|
||||
logViewer.empty();
|
||||
|
||||
if ($scope.terminal) {
|
||||
$scope.terminal.destroy();
|
||||
$scope.terminal = null;
|
||||
}
|
||||
|
||||
if ($scope.terminalSocket) {
|
||||
$scope.terminalSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.restartApp = function () {
|
||||
$scope.restartAppBusy = true;
|
||||
var appId = $scope.selected.value;
|
||||
|
||||
function waitUntilStopped(callback) {
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
Client.getApp(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.runState === 'stopped') return callback();
|
||||
setTimeout(waitUntilStopped.bind(null, callback), 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.stopApp(appId, function (error) {
|
||||
if (error) return console.error('Failed to stop app.', error);
|
||||
|
||||
waitUntilStopped(function (error) {
|
||||
if (error) return console.error('Failed to get app status.', error);
|
||||
|
||||
Client.startApp(appId, function (error) {
|
||||
if (error) console.error('Failed to start app.', error);
|
||||
|
||||
$scope.restartAppBusy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showLogs = function () {
|
||||
$scope.terminalVisible = false;
|
||||
|
||||
reset();
|
||||
|
||||
if (!$scope.selected) return;
|
||||
|
||||
var func = $scope.selected.type === 'platform' ? Client.getPlatformLogs : Client.getAppLogs;
|
||||
func($scope.selected.value, true, $scope.lines, function handleLogs(error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.activeEventSource = result;
|
||||
result.onmessage = function handleMessage(message) {
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
} catch (e) {
|
||||
return console.error(e);
|
||||
}
|
||||
|
||||
// check if we want to auto scroll (this is before the appending, as that skews the check)
|
||||
var tmp = $('.logs-and-term-container');
|
||||
var autoScroll = tmp[0].scrollTop > (tmp[0].scrollTopMax - 24);
|
||||
|
||||
var logLine = $('<div class="log-line">');
|
||||
var timeString = moment.utc(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss');
|
||||
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(typeof data.message === 'string' ? data.message : ab2str(data.message)));
|
||||
tmp.append(logLine);
|
||||
|
||||
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showTerminal = function (retry) {
|
||||
$scope.terminalVisible = true;
|
||||
|
||||
reset();
|
||||
|
||||
if (!$scope.selected) return;
|
||||
|
||||
// we can only connect to apps here
|
||||
if ($scope.selected.type !== 'app') {
|
||||
var tmp = $('.logs-and-term-container');
|
||||
var logLine = $('<div class="log-line">');
|
||||
logLine.html('Terminal is only supported for apps, not for ' + $scope.selected.name);
|
||||
tmp.append(logLine);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.terminal = new Terminal();
|
||||
$scope.terminal.open(document.querySelector('.logs-and-term-container'));
|
||||
$scope.terminal.fit();
|
||||
|
||||
try {
|
||||
// websocket cannot use relative urls
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
|
||||
$scope.terminalSocket = new WebSocket(url);
|
||||
$scope.terminal.attach($scope.terminalSocket);
|
||||
|
||||
$scope.terminalSocket.onclose = function () {
|
||||
// retry in one second only if terminal view is still selected
|
||||
$scope.terminalReconnectTimeout = setTimeout(function () {
|
||||
// if the scope was already destroyed, do not reconnect
|
||||
if ($scope.$$destroyed) return;
|
||||
if ($scope.terminalVisible) $scope.showTerminal(true);
|
||||
}, 1000);
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (retry) $scope.terminal.writeln('Reconnecting...');
|
||||
else $scope.terminal.writeln('Connecting...');
|
||||
};
|
||||
|
||||
$scope.terminalInject = function (addon) {
|
||||
if (!$scope.terminalSocket) return;
|
||||
|
||||
var cmd;
|
||||
if (addon === 'mysql') cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
|
||||
else if (addon === 'postgresql') cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
|
||||
else if (addon === 'mongodb') cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
|
||||
else if (addon === 'redis') cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
|
||||
|
||||
if (!cmd) return;
|
||||
|
||||
cmd += ' ';
|
||||
|
||||
$scope.terminalSocket.send(cmd);
|
||||
$scope.terminal.focus();
|
||||
}
|
||||
|
||||
$scope.$watch('selected', function (newVal) {
|
||||
if (!newVal) return;
|
||||
|
||||
if ($scope.terminalVisible) $scope.showTerminal();
|
||||
else $scope.showLogs();
|
||||
});
|
||||
|
||||
Client.onReady($scope.populateLogTypes);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
if ($scope.activeEventSource) {
|
||||
$scope.activeEventSource.onmessage = function () {};
|
||||
$scope.activeEventSource.close();
|
||||
$scope.activeEventSource = null;
|
||||
}
|
||||
|
||||
if ($scope.terminal) {
|
||||
$scope.terminal.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// terminal right click handling
|
||||
$scope.terminalClear = function () {
|
||||
if (!$scope.terminal) return;
|
||||
$scope.terminal.clear();
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
$scope.terminalCopy = function () {
|
||||
if (!$scope.terminal) return;
|
||||
|
||||
// execCommand('copy') would copy any selection from the page, so do this only if terminal has a selection
|
||||
if (!$scope.terminal.getSelection()) return;
|
||||
|
||||
document.execCommand('copy');
|
||||
$scope.terminal.focus();
|
||||
};
|
||||
|
||||
$('.contextMenuBackdrop').on('click', function (e) {
|
||||
$('#terminalContextMenu').hide();
|
||||
$('.contextMenuBackdrop').hide();
|
||||
|
||||
$scope.terminal.focus();
|
||||
});
|
||||
|
||||
$('.logs-and-term-container').on('contextmenu', function (e) {
|
||||
if (!$scope.terminal) return true;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
$('.contextMenuBackdrop').show();
|
||||
$('#terminalContextMenu').css({
|
||||
display: 'block',
|
||||
left: e.pageX,
|
||||
top: e.pageY
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['downloadFileModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
+159
-167
@@ -1,211 +1,203 @@
|
||||
|
||||
<!-- Modal enable email -->
|
||||
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Cloudron Email Server</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
|
||||
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean'">
|
||||
The Cloudron will setup Email related DNS records automatically.
|
||||
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
|
||||
<br/><br/>
|
||||
Disabling Cloudron Email later will <b>not</b> put the old records back.
|
||||
<br/><br/>
|
||||
Status of DNS Records will show an error when DNS is propagating (~5 minutes).
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="email.enable()">I understand, enable</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Cloudron Email Server</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
|
||||
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean' || dnsConfig.provider === 'cloudflare'">
|
||||
Cloudron will setup Email related DNS records automatically.
|
||||
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
|
||||
<br/><br/>
|
||||
Disabling Cloudron Email later will <b>not</b> put the old records back.
|
||||
<br/><br/>
|
||||
Status of DNS Records will show an error while DNS is propagating (~5 minutes).
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="email.enable()">I understand, enable</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Email</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Email</h1>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>IMAP and SMTP Server</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3>IMAP and SMTP Server</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
Cloudron has a built-in <a ng-href="{{ config.webServerOrigin + '/documentation/email/' }}" target="_blank">email server</a> that allows users to send and receive email for your domain.
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
Cloudron has a built-in <a ng-href="{{ config.webServerOrigin + '/documentation/email/' }}" target="_blank">email server</a> that allows users to send and receive email for your domain.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="mailConfig.enabled">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#mail_settings">Mail server settings for email clients</a>
|
||||
<div id="mail_settings" class="panel-collapse collapse">
|
||||
<br/>
|
||||
<p><b>Incoming Mail (IMAP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 993 (TLS)</p>
|
||||
<p><b>Outgoing Mail (SMTP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 587 (STARTTLS)</p>
|
||||
<p><b>ManageSieve</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 4190 (TLS)</p>
|
||||
<p>All the servers require your Cloudron credentials for authentication.</p>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#mail_settings">Mail server settings for email clients</a>
|
||||
<div id="mail_settings" class="panel-collapse collapse">
|
||||
<br/>
|
||||
<p><b>Incoming Mail (IMAP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 993 (TLS)</p>
|
||||
<p><b>Outgoing Mail (SMTP)</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 587 (STARTTLS)</p>
|
||||
<p><b>ManageSieve</b><br/>Server: <span ng-click-select>my.{{config.fqdn}}</span><br/>Port: 4190 (TLS)</p>
|
||||
<p>All the servers require your Cloudron credentials for authentication.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
|
||||
<button ng-class="mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
|
||||
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a ng-href="{{ config.webServerOrigin + '/documentation/managed-hosting/#using-a-custom-domain' }}" target="_blank">custom domain</a>.</span>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
|
||||
<button ng-class="mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
|
||||
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a ng-href="{{ config.webServerOrigin + '/documentation/managed-hosting/#using-a-custom-domain' }}" target="_blank">custom domain</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="isPaying">
|
||||
<div class="text-left">
|
||||
<h3>Outbound Mail Relay</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left" ng-show="isPaying">
|
||||
<h3>Outbound Mail Relay</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="isPaying">
|
||||
<div class="row">
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="isPaying">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
Select the mail server through which Cloudron will send outbound mails:
|
||||
Select the mail server through which Cloudron will send outbound mails:
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="mailRelay.preset.provider !== 'cloudron-smtp'">
|
||||
</div>
|
||||
<div class="row" ng-show="mailRelay.preset.provider !== 'cloudron-smtp'">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">SMTP Port (STARTTLS)</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.password" name="password" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">SMTP Port (STARTTLS)</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.password" name="password" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary" ng-click="mailRelay.submit()" ng-disabled="(mailRelay.preset.provider !== 'cloudron-smtp' && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailRelay.busy"></i> Save</button>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary" ng-click="mailRelay.submit()" ng-disabled="(mailRelay.preset.provider !== 'cloudron-smtp' && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailRelay.busy"></i> Save</button>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="mailConfig.enabled && isPaying">
|
||||
<div class="text-left">
|
||||
<h3>Catch-all</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left" ng-show="mailConfig.enabled && isPaying">
|
||||
<h3>Catch-all</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="mailConfig.enabled && isPaying">
|
||||
<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 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 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>
|
||||
|
||||
<div class="section-header" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="text-left">
|
||||
<h3>DNS Records</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<h3>DNS Records</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
Set the following DNS records to guarantee email delivery:
|
||||
<div class="col-md-12">
|
||||
Set the following DNS records to guarantee email delivery:
|
||||
|
||||
<br/><br/>
|
||||
<br/><br/>
|
||||
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p>Domain: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>Record type: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">Expected value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">Current value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : '[not set]' }}</tt></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
Outbound SMTP
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_port" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ relay.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p>Domain: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>Record type: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">Expected value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">Current value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : '[not set]' }}</tt></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
Outbound SMTP
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_port" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ relay.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="content">
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h2>Memory</h2>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<div class="logs-main">
|
||||
|
||||
<div class="logs-header">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="text-left">
|
||||
<h1>Logs</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-controls">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="filter">
|
||||
<select class="form-control" ng-options="log.name for log in logs track by log.value" ng-model="selected"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<a class="btn btn-default btn-outline" ng-href="{{ selected.url }}&format=short&lines=800"><i class="fa fa-download"></i> Download </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1 log-line-container"></div>
|
||||
|
||||
</div>
|
||||
@@ -1,81 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global moment */
|
||||
|
||||
angular.module('Application').controller('LogsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.logs = [];
|
||||
$scope.selected = '';
|
||||
$scope.activeEventSource = null;
|
||||
$scope.lines = 10;
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
$scope.populateLogTypes = function () {
|
||||
$scope.logs.push({ name: 'System (All)', type: 'platform', value: 'all', url: Client.makeURL('/api/v1/cloudron/logs?units=all') });
|
||||
$scope.logs.push({ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs?units=box') });
|
||||
$scope.logs.push({ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs?units=mail') });
|
||||
|
||||
Client.getInstalledApps().forEach(function (app) {
|
||||
$scope.logs.push({ name: app.fqdn + ' (' + app.manifest.title + ')', type: 'app', value: app.id, url: Client.makeURL('/api/v1/apps/' + app.id + '/logs') });
|
||||
});
|
||||
|
||||
$scope.selected = $scope.logs[0];
|
||||
};
|
||||
|
||||
$scope.$watch('selected', function (newVal) {
|
||||
if (!newVal) return;
|
||||
|
||||
// close the old event source so we wont receive any new logs
|
||||
if ($scope.activeEventSource) {
|
||||
$scope.activeEventSource.close();
|
||||
$scope.activeEventSource = null;
|
||||
}
|
||||
|
||||
var func = newVal.type === 'platform' ? Client.getPlatformLogs : Client.getAppLogs;
|
||||
func(newVal.value, true, $scope.lines, function handleLogs(error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
var logViewer = $('.log-line-container');
|
||||
logViewer.empty();
|
||||
|
||||
$scope.activeEventSource = result;
|
||||
result.onmessage = function handleMessage(message) {
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
} catch (e) {
|
||||
return console.error(e);
|
||||
}
|
||||
|
||||
// check if we want to auto scroll (this is before the appending, as that skews the check)
|
||||
var tmp = document.querySelector('.log-line-container');
|
||||
var autoScroll = tmp.scrollTop > (tmp.scrollTopMax - 24);
|
||||
|
||||
var logLine = $('<div class="log-line">');
|
||||
var timeString = moment.utc(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss');
|
||||
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(typeof data.message === 'string' ? data.message : ab2str(data.message)));
|
||||
logViewer.append(logLine);
|
||||
|
||||
if (autoScroll) tmp.lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Client.onReady($scope.populateLogTypes);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
if ($scope.activeEventSource) {
|
||||
$scope.activeEventSource.onmessage = function () {};
|
||||
$scope.activeEventSource.close();
|
||||
$scope.activeEventSource = null;
|
||||
}
|
||||
});
|
||||
}]);
|
||||
+228
-257
@@ -54,33 +54,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable email -->
|
||||
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Cloudron Email Server</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
|
||||
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean'">
|
||||
The Cloudron will setup Email related DNS records automatically.
|
||||
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
|
||||
<br/><br/>
|
||||
Disabling Cloudron Email later will <b>not</b> put the old records back.
|
||||
<br/><br/>
|
||||
Status of DNS Records will show an error when DNS is propagating (~5 minutes).
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="email.enable()">I understand, enable</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup failed -->
|
||||
<div class="modal fade" id="createBackupFailedModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -156,6 +129,13 @@
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">
|
||||
This option breaks the backup and restore functionality of Cloudron and should only be used for testing. Please make sure the server is completely backed up using alternate means.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
@@ -221,234 +201,225 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="avatarChange.showChangeAvatar()" style="background-image: url('{{ client.avatar }}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="cloudronNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr ng-show="config.provider === 'caas'">
|
||||
<td class="text-muted" style="vertical-align: top;">Model</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.provider }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="config.provider === 'caas'">
|
||||
<div class="text-left">
|
||||
<h3>Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="config.provider === 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-right">
|
||||
<a href="{{ config.webServerOrigin }}/console.html#/userprofile?view=credit_card" target="_blank">Change payment method</a>
|
||||
or
|
||||
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-10 plans" style="margin-left: 20px">
|
||||
<div ng-repeat="plan in availablePlans">
|
||||
<label>
|
||||
<input type="radio" ng-model="planChange.requestedPlan" ng-value="plan">
|
||||
{{ plan.name }} ({{ plan.slug | uppercase }}) - {{ plan.price/100 }}{{ currency }}/month
|
||||
<span ng-show="currentPlan.name === plan.name" style="font-weight: bold"> (current plan)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-right" ng-disabled="planChange.requestedPlan.name === currentPlan.name" ng-click="planChange.showChangePlan()">Change Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="text-left">
|
||||
<h3>Cloudron.io Account</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row" ng-show="currentSubscription.plan.id === 'free' || currentSubscription.plan.id === 'undecided'">
|
||||
<div class="col-xs-12">
|
||||
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/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Account Email</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?email=' + appstoreConfig.profile.email }}" target="_blank">{{ appstoreConfig.profile.email }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Cloudron ID</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ appstoreConfig.cloudronId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Subscription</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ currentSubscription.plan.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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 }}" 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>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Backups</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.provider === 'caas' ? 'cloudron.io' : backupConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Location</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder || '/var/backups' }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio' || backupConfig.provider === 'exoscale-sos'">{{ backupConfig.bucket + '/' + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + '/' + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Last backup</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="lastBackup">{{ lastBackup.creationTime | prettyDate }}</span>
|
||||
<span ng-hide="lastBackup">No backups have been made yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.doCreateBackup()" ng-disabled="createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Updates</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Configure the update schedule for the platform and the apps</p>
|
||||
<p class="text-danger" ng-show="autoUpdate.error"><br/>{{ autoUpdate.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * *">
|
||||
Every night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * 6">
|
||||
Saturday night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="never">
|
||||
Update manually (Not recommended)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="autoUpdate.busy"></i>
|
||||
<span class="text-success text-bold" ng-show="autoUpdate.success && autoUpdate.pattern === autoUpdate.currentPattern">Saved</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="autoUpdate.submit()" ng-disabled="autoUpdate.busy || autoUpdate.pattern === autoUpdate.currentPattern"> Save</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="autoUpdate.checkNow()" ng-disabled="autoUpdate.busy" style="margin-right: 10px">Check now</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="avatarChange.showChangeAvatar()" style="background-image: url('{{ client.avatar }}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="cloudronNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr ng-show="config.provider === 'caas'">
|
||||
<td class="text-muted" style="vertical-align: top;">Model</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.provider }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="config.provider === 'caas'">
|
||||
<h3>Plans</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="config.provider === 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-right">
|
||||
<a href="{{ config.webServerOrigin }}/console.html#/userprofile?view=credit_card" target="_blank">Change payment method</a>
|
||||
or
|
||||
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-10 plans" style="margin-left: 20px">
|
||||
<div ng-repeat="plan in availablePlans">
|
||||
<label>
|
||||
<input type="radio" ng-model="planChange.requestedPlan" ng-value="plan">
|
||||
{{ plan.name }} ({{ plan.slug | uppercase }}) - {{ plan.price/100 }}{{ currency }}/month
|
||||
<span ng-show="currentPlan.name === plan.name" style="font-weight: bold"> (current plan)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-right" ng-disabled="planChange.requestedPlan.name === currentPlan.name" ng-click="planChange.showChangePlan()">Change Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="backupConfig.provider !== 'caas'">
|
||||
<h3>Cloudron.io Account</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row" ng-show="currentSubscription.plan.id === 'free' || currentSubscription.plan.id === 'undecided'">
|
||||
<div class="col-xs-12">
|
||||
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/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Account Email</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?email=' + appstoreConfig.profile.email }}" target="_blank">{{ appstoreConfig.profile.email }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Cloudron ID</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ appstoreConfig.cloudronId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Subscription</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ currentSubscription.plan.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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 }}" 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>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Backups</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.provider === 'caas' ? 'cloudron.io' : backupConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Location</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder || '/var/backups' }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio' || backupConfig.provider === 'exoscale-sos'">{{ backupConfig.bucket + '/' + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + '/' + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Last backup</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="lastBackup">{{ lastBackup.creationTime | prettyDate }}</span>
|
||||
<span ng-hide="lastBackup">No backups have been made yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.doCreateBackup()" ng-disabled="createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Updates</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Configure the update schedule for the platform and the apps</p>
|
||||
<p class="text-danger" ng-show="autoUpdate.error"><br/>{{ autoUpdate.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * *">
|
||||
Every night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="00 00 1,3,5,23 * * 6">
|
||||
Saturday night
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="scheduleRadio" ng-change="autoUpdate.success = false" ng-model="autoUpdate.pattern" value="never">
|
||||
Update manually (Not recommended)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="autoUpdate.busy"></i>
|
||||
<span class="text-success text-bold" ng-show="autoUpdate.success && autoUpdate.pattern === autoUpdate.currentPattern">Saved</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="autoUpdate.submit()" ng-disabled="autoUpdate.busy || autoUpdate.pattern === autoUpdate.currentPattern"> Save</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="autoUpdate.checkNow()" ng-disabled="autoUpdate.busy" style="margin-right: 10px">Check now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,84 +1,79 @@
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Support</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Documentation and Chat</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h1>Support</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
For user manuals and app development related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
|
||||
Cloudron is <a href="https://git.cloudron.io" target="_blank">open source</a> - use the <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">issue tracker</a>
|
||||
to report bugs and raise feature requests.
|
||||
<br/><br/>
|
||||
For any other questions, chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3>Documentation and Chat</h3>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Feedback</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
For user manuals and app development related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
|
||||
Cloudron is <a href="https://git.cloudron.io" target="_blank">open source</a> - use the <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">issue tracker</a>
|
||||
to report bugs and raise feature requests.
|
||||
<br/><br/>
|
||||
For any other questions, chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
We would love to hear your feedback. Help us improve our product by reporting any bugs or feature requests.
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<div class="form-group">
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required>
|
||||
<option value="feedback">Enhancement / Idea</option>
|
||||
<option value="ticket">Bug Report</option>
|
||||
<option value="app_missing">Missing App</option>
|
||||
<option value="app_error">App Error/Failing</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
|
||||
<input type="text" class="form-control" name="subject" placeholder="Enter your idea or issue" ng-model="feedback.subject" ng-maxlength="512" ng-minlength="1" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.description.$dirty && feedbackForm.description.$invalid) }">
|
||||
<textarea class="form-control" name="description" rows="3" placeholder="Describe your idea or issue" ng-model="feedback.description" ng-minlength="1" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="feedback.busy"></i> Submit</button>
|
||||
<span ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</span>
|
||||
<span ng-show="feedback.success" class="text-success text-bold">Thank You!</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3>Feedback</h3>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Remote Support</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
We would love to hear your feedback. Help us improve our product by reporting any bugs or feature requests.
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<div class="form-group">
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required>
|
||||
<option value="feedback">Enhancement / Idea</option>
|
||||
<option value="ticket">Bug Report</option>
|
||||
<option value="app_missing">Missing App</option>
|
||||
<option value="app_error">App Error/Failing</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
|
||||
<input type="text" class="form-control" name="subject" placeholder="Enter your idea or issue" ng-model="feedback.subject" ng-maxlength="512" ng-minlength="1" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.description.$dirty && feedbackForm.description.$invalid) }">
|
||||
<textarea class="form-control" name="description" rows="3" placeholder="Describe your idea or issue" ng-model="feedback.description" ng-minlength="1" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="feedback.busy"></i> Submit</button>
|
||||
<span ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</span>
|
||||
<span ng-show="feedback.success" class="text-success text-bold">Thank You!</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="config.provider !== 'caas' && user.admin">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
Enable this option to allow Cloudron engineers to connect to this server via SSH.
|
||||
<br/>
|
||||
<br/>
|
||||
Do not enable this option before contacting us first at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
|
||||
<br/>
|
||||
<br/>
|
||||
<button class="btn" ng-class="{ 'btn-danger': !sshSupportEnabled, 'btn-primary': sshSupportEnabled }" ng-click="toggleSshSupport()">{{ sshSupportEnabled ? 'Disable SSH support access' : 'Enable SSH support access' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left" ng-show="config.provider !== 'caas' && user.admin">
|
||||
<h3>Remote Support</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="config.provider !== 'caas' && user.admin">
|
||||
<div class="grid-item-top">
|
||||
<div class="row animateMeOpacity">
|
||||
<div class="col-lg-12">
|
||||
Enable this option to allow Cloudron engineers to connect to this server via SSH.
|
||||
<br/>
|
||||
<br/>
|
||||
Do not enable this option before contacting us first at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
|
||||
<br/>
|
||||
<br/>
|
||||
<button class="btn" ng-class="{ 'btn-danger': !sshSupportEnabled, 'btn-primary': sshSupportEnabled }" ng-click="toggleSshSupport()">{{ sshSupportEnabled ? 'Disable SSH support access' : 'Enable SSH support access' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,84 +78,80 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Access Tokens <button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="tokenAdd.show(apiClient)"><i class="fa fa-plus"></i> New Token</button> </h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These tokens can be used to access the <a ng-href="{{ config.webServerOrigin + '/documentation/developer/api/' }}" target="_blank">Cloudron API</a>.</p>
|
||||
<br/>
|
||||
<h4 class="text-muted">Active Tokens</h4>
|
||||
<hr/>
|
||||
<p ng-repeat="token in apiClient.activeTokens">
|
||||
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>OAuth Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New OAuth Client</button></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
|
||||
<div class="card" ng-repeat="client in activeClients | activeOAuthClients:user">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h4 class="text-muted">
|
||||
{{client.name}} <span ng-show="client.type !== 'external' && client.type !== 'built-in'">on {{client.location}}{{ config.isCustomDomain ? '.' : '-' }}{{config.fqdn}}</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<b>{{ client.activeTokens.length }}</b> active token(s).
|
||||
<br/>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
|
||||
<div id="collapse{{client.id}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove OAuth Client" ng-show="client.type === 'external'">Remove OAuth Client</button></h4>
|
||||
<hr/>
|
||||
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
|
||||
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>
|
||||
<p>Client ID: <b ng-click-select>{{ client.id }}</b></p>
|
||||
<p ng-show="client.clientSecret" style="overflow: auto; white-space: nowrap;">Client Secret: <b ng-click-select>{{ client.clientSecret }}</b></p>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4 class="text-muted">Tokens
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-xs btn-default" ng-click="removeAccessTokens(client)" ng-disabled="!client.activeTokens.length || client.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="client.busy"></i> Revoke All</button>
|
||||
<button class="btn btn-xs btn-primary btn-outline" ng-click="tokenAdd.show(client)"><i class="fa fa-plus"></i> New Token</button>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p ng-repeat="token in client.activeTokens">
|
||||
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(client, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h3>Access Tokens <button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="tokenAdd.show(apiClient)"><i class="fa fa-plus"></i> New Token</button> </h3>
|
||||
</div>
|
||||
|
||||
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These tokens can be used to access the <a ng-href="{{ config.webServerOrigin + '/documentation/developer/api/' }}" target="_blank">Cloudron API</a>.</p>
|
||||
<br/>
|
||||
<h4 class="text-muted">Active Tokens</h4>
|
||||
<hr/>
|
||||
<p ng-repeat="token in apiClient.activeTokens">
|
||||
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>OAuth Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New OAuth Client</button></h3>
|
||||
</div>
|
||||
|
||||
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
|
||||
<div class="card" ng-repeat="client in activeClients | activeOAuthClients:user">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h4 class="text-muted">
|
||||
{{client.name}} <span ng-show="client.type !== 'external' && client.type !== 'built-in'">on {{client.location}}{{ config.isCustomDomain ? '.' : '-' }}{{config.fqdn}}</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<b>{{ client.activeTokens.length }}</b> active token(s).
|
||||
<br/>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
|
||||
<div id="collapse{{client.id}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove OAuth Client" ng-show="client.type === 'external'">Remove OAuth Client</button></h4>
|
||||
<hr/>
|
||||
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
|
||||
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>
|
||||
<p>Client ID: <b ng-click-select>{{ client.id }}</b></p>
|
||||
<p ng-show="client.clientSecret" style="overflow: auto; white-space: nowrap;">Client Secret: <b ng-click-select>{{ client.clientSecret }}</b></p>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4 class="text-muted">Tokens
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-xs btn-default" ng-click="removeAccessTokens(client)" ng-disabled="!client.activeTokens.length || client.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="client.busy"></i> Revoke All</button>
|
||||
<button class="btn btn-xs btn-primary btn-outline" ng-click="tokenAdd.show(client)"><i class="fa fa-plus"></i> New Token</button>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p ng-repeat="token in client.activeTokens">
|
||||
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(client, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,106 +231,106 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Users <button class="btn btn-primary btn-outline pull-right" ng-click="useradd.show()"><i class="fa fa-user-plus"></i> New User</button></h1>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h1>Users <button class="btn btn-primary btn-outline pull-right" ng-click="useradd.show()"><i class="fa fa-user-plus"></i> New User</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<span ng-show="mailConfig.enabled">
|
||||
Each user has a mailbox at <b><i>username</i>@{{ config.fqdn }}</b>.
|
||||
Please refer to the <a ng-href="{{ config.webServerOrigin + '/documentation/email/#imap-settings-for-cloudron-email' }}" target="_blank">user documentation</a> on how to use Cloudron email accounts.
|
||||
<br/>
|
||||
<br/>
|
||||
</span>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1px;"></th>
|
||||
<th style="">User</th>
|
||||
<th style="" class="text-left hidden-xs hidden-sm">Groups</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>
|
||||
<i class="fa fa-briefcase arrow" ng-show="user.admin" uib-tooltip="This user is an admin and can manage apps, groups and other users"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-show="user.username">
|
||||
{{ user.username }} <span class="text-muted" ng-hide="mailConfig.enabled">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-hide="user.username">
|
||||
<span class="text-muted" uib-tooltip="User is not activated yet">{{ user.alternateEmail || user.email }}</span>
|
||||
</td>
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="useredit.show(user)">
|
||||
<span class="group-badge" ng-repeat="groupId in user.groupIds | ignoreAdminGroup">
|
||||
{{ groupsById[groupId].name }}
|
||||
</span>
|
||||
</td>
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<span ng-show="mailConfig.enabled">
|
||||
Each user has a mailbox at <b><i>username</i>@{{ config.fqdn }}</b>.
|
||||
Please refer to the <a ng-href="{{ config.webServerOrigin + '/documentation/email/#imap-settings-for-cloudron-email' }}" target="_blank">user documentation</a> on how to use Cloudron email accounts.
|
||||
<br/>
|
||||
<br/>
|
||||
</span>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1px;"></th>
|
||||
<th style="">User</th>
|
||||
<th style="" class="text-left hidden-xs hidden-sm">Groups</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>
|
||||
<i class="fa fa-briefcase arrow" ng-show="user.admin" uib-tooltip="This user is an admin and can manage apps, groups and other users"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-show="user.username">
|
||||
{{ user.username }} <span class="text-muted" ng-hide="mailConfig.enabled">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="useredit.show(user)" ng-hide="user.username">
|
||||
<span class="text-muted" uib-tooltip="User is not activated yet">{{ user.alternateEmail || user.email }}</span>
|
||||
</td>
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="useredit.show(user)">
|
||||
<span class="group-badge" ng-repeat="groupId in user.groupIds | ignoreAdminGroup">
|
||||
{{ groupsById[groupId].name }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-default" ng-click="sendInvite(user)" title="Send invitation email"><i class="fa fa-paper-plane-o"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="useredit.show(user)" title="Edit User Profile"><i class="fa fa-pencil"></i></button>
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-danger" ng-click="userremove.show(user)" title="Remove User"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-default" ng-click="sendInvite(user)" title="Send invitation email"><i class="fa fa-paper-plane-o"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="useredit.show(user)" title="Edit User Profile"><i class="fa fa-pencil"></i></button>
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-danger" ng-click="userremove.show(user)" title="Remove User"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Groups <button class="btn btn-primary btn-outline pull-right" ng-click="groupAdd.show()"><i class="fa fa-plus"></i> New Group</button></h1>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h1>Groups <button class="btn btn-primary btn-outline pull-right" ng-click="groupAdd.show()"><i class="fa fa-plus"></i> New Group</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
Groups can be used to control access to an app.
|
||||
<span ng-show="mailConfig.enabled">Each group serves as an email list at <b><i>groupname</i>@{{ config.fqdn }}</b>. Any email sent to this address will be forwarded to each group member.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="">Name</th>
|
||||
<th style="width: 300px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<td class="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ group.name }}
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" title="Remove Group"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
Groups can be used to control access to an app.
|
||||
<span ng-show="mailConfig.enabled">Each group serves as an email list at <b><i>groupname</i>@{{ config.fqdn }}</b>. Any email sent to this address will be forwarded to each group member.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="">Name</th>
|
||||
<th style="width: 300px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<td class="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ group.name }}
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" title="Remove Group"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user