Compare commits

..

75 Commits

Author SHA1 Message Date
Girish Ramakrishnan 61f7c1af48 Remove unused error codes 2017-08-28 15:27:17 -07:00
Girish Ramakrishnan 00786dda05 Do not crash if DNS creds do not work during startup
If DNS creds are invalid, then platform.start() keeps crashing on a
mail container update. For now, just log the error and move on.

Part of #406
2017-08-28 14:55:36 -07:00
Girish Ramakrishnan 8b9f44addc 1.6.3 changes 2017-08-28 13:49:15 -07:00
Johannes Zellner 56c7dbb6e4 Do not attempt to reconnect if the debug view is already gone
Fixes #408
2017-08-28 21:06:25 +02:00
Girish Ramakrishnan c47f878203 Set priority for MX records
Fixes #410
2017-08-26 15:54:38 -07:00
Girish Ramakrishnan 8a2107e6eb Show email text for Cloudflare 2017-08-26 15:37:24 -07:00
Girish Ramakrishnan cd9f0f69d8 email dialog has moved to it's own view 2017-08-26 15:36:12 -07:00
Girish Ramakrishnan 1da91b64f6 Filter out possibly sensitive information for normal users
Fixes #407
2017-08-26 14:47:51 -07:00
Johannes Zellner a87dd65c1d Workaround for firefox flexbox bug
Fixes selection while clicking on empty flexbox space.

This only happens in firefox and seems to be a bug in firefox
flexbox implementation, where the first child element with a
non zero size, in a flexbox managed `block` element, has the
`float` property.

Fixes #405
2017-08-24 23:29:42 +02:00
Johannes Zellner 7c63d9e758 Fix typo in css 2017-08-24 23:16:36 +02:00
Girish Ramakrishnan 329bf596ac Indicate that directories can be downloaded 2017-08-24 13:38:50 -07:00
Girish Ramakrishnan 2a57c4269a handle app not found 2017-08-23 13:23:04 -07:00
Girish Ramakrishnan ca8813dce3 1.6.2 changes 2017-08-23 10:43:27 -07:00
Girish Ramakrishnan 3aebf51360 Fix upload of large files to apps
6a0ef7a1c1 broke the upload for apps

e2e test is being added
2017-08-23 10:22:54 -07:00
Johannes Zellner 103f8db8cb Do not expand to fixed pixel size on mobile 2017-08-23 16:57:34 +02:00
Johannes Zellner 04c127b78d Add changes for 1.6.1
Due to regressions we should skip 1.6.0 thus the same changelog
2017-08-23 16:14:30 +02:00
Johannes Zellner 9bef1bcf64 Hijack and demux the container exec stream to be compliant with new
dockerode
2017-08-23 16:04:50 +02:00
Johannes Zellner 718413c089 autocomplete attribute is not respected for username/password fields
Since the cloudflare email input field is above the password field
some browsers will automatically autofill it with the username
as it looks like a login form. So we add a hidden unused input field
which gets autofilled instead :-/
2017-08-23 13:13:00 +02:00
Girish Ramakrishnan a34691df44 Hide the header as well 2017-08-22 09:30:18 -07:00
Girish Ramakrishnan 795e38fe82 file is an object 2017-08-22 09:15:46 -07:00
Johannes Zellner 1d348fb0f3 Do not lose focus on terminal 2017-08-22 16:24:26 +02:00
Johannes Zellner 91f3318879 Implement rightclick menu for terminal text copy 2017-08-22 16:23:06 +02:00
Girish Ramakrishnan c61808f4c6 1.6.0 changes 2017-08-21 16:08:37 -07:00
Girish Ramakrishnan 991b2dad28 bump mail container version
part of #400
2017-08-21 15:54:21 -07:00
Girish Ramakrishnan f3d9a70de7 Only send the stdout stream 2017-08-21 10:46:13 -07:00
Johannes Zellner 60758de10a Fixup package.json linter issues and clean the shrinkwrap 2017-08-21 12:45:15 +02:00
Girish Ramakrishnan 6a0ef7a1c1 Allow larger files to be uploaded
Note that other upload APIs like avatar are still limited to 1m by
the nginx config
2017-08-20 19:15:54 -07:00
Girish Ramakrishnan 7cb451c157 Allow dirs to downloaded as tarballs 2017-08-20 18:54:59 -07:00
Girish Ramakrishnan 3c31c96ad4 Hide the download dialog after download starts 2017-08-20 18:29:11 -07:00
Johannes Zellner 5d73f58631 Show upload progress 2017-08-20 19:32:00 +02:00
Johannes Zellner 4ca7cccdae Give error feedback if the requested file does not exist 2017-08-20 18:50:37 +02:00
Johannes Zellner 82380b6b7c Remove hardcoded /app/data and fix submit for file downloads 2017-08-20 18:09:43 +02:00
Johannes Zellner 979c4e77e3 Fix view bug when terminal reconnects but user has moved on 2017-08-20 18:00:07 +02:00
Johannes Zellner e318fb0c01 Show restat button also in logs view 2017-08-20 17:57:32 +02:00
Girish Ramakrishnan 77d2fb97e5 test: create logrotate dir 2017-08-19 18:57:43 -07:00
Girish Ramakrishnan 24e6c4d963 bump test image 2017-08-19 17:57:21 -07:00
Girish Ramakrishnan 064c5cf7f2 Fix failing test 2017-08-19 17:41:15 -07:00
Girish Ramakrishnan 891542bfb9 move restart button 2017-08-19 17:33:59 -07:00
Girish Ramakrishnan 599702d410 Fix casing 2017-08-19 16:45:20 -07:00
Girish Ramakrishnan 3cb39754fd Make logs button work for apps 2017-08-19 12:52:48 -07:00
Girish Ramakrishnan f04345a99a Move restart button to log view 2017-08-19 12:49:03 -07:00
Johannes Zellner 3d59b8a5b0 Deliver content-length and file not found errors for file downloads 2017-08-19 12:13:04 +02:00
Johannes Zellner cf518b0285 Resize terminal based on initial DOM size
Currently we cannot send new cols,rows on DOM element resize
as they are sent on connect only and a reconnect would loose
current session
2017-08-19 11:32:00 +02:00
Girish Ramakrishnan 52832c881a Add upload and download for the webterminal 2017-08-18 21:19:48 -07:00
Girish Ramakrishnan 537fbff4aa Use ws directly to handle new exec ws route 2017-08-18 19:46:18 -07:00
Johannes Zellner e3040b334d Do not submit injected commands right away but give some space and fix
focus
2017-08-18 20:36:52 +02:00
Johannes Zellner 6c2879d567 Rename debug view to terminal and logs 2017-08-18 20:36:47 +02:00
Johannes Zellner 595c89076f Add postgres, mongo and redis client injection 2017-08-18 11:26:10 -07:00
Johannes Zellner c85f5b15c6 Reenable custom tcp upgrade handling 2017-08-18 11:26:05 -07:00
Johannes Zellner 8fbed7e84b Ensure we only write to the websocket if it is open 2017-08-18 11:26:00 -07:00
Johannes Zellner ee3c5f67af Show mysql addon only if the app uses it 2017-08-18 11:25:54 -07:00
Johannes Zellner 52db28e876 Verify the websocket request 2017-08-18 11:25:49 -07:00
Johannes Zellner 65bc3491f6 enable timeout middleware again and reset it for all upgrade requests 2017-08-18 11:25:45 -07:00
Johannes Zellner 82f512dc27 Rename logs view to debug view 2017-08-18 11:25:37 -07:00
Johannes Zellner 4b41378d08 Ensure app restarts also close the websocket 2017-08-18 11:25:05 -07:00
Johannes Zellner 1fd4e27d92 Fix logs autoscroll 2017-08-18 11:25:00 -07:00
Johannes Zellner 2420fef6b1 Reconnect the terminal on disconnection
This can happen if the app crashes or restarts
2017-08-18 11:24:55 -07:00
Johannes Zellner 50074b936a Integrate the terminal with the logs ui 2017-08-18 11:24:48 -07:00
Johannes Zellner f98e68edc1 Add express-ws node module 2017-08-18 11:24:42 -07:00
Johannes Zellner 83e5daf08c Add xterm.js 2017-08-18 11:24:34 -07:00
Girish Ramakrishnan 53b43ca36b Don't show restore button for noop backend 2017-08-17 20:20:06 -07:00
Girish Ramakrishnan d11842a7f8 Show popup when using noop backend 2017-08-17 19:52:08 -07:00
Girish Ramakrishnan 6746781b46 Add warning for noop backend
Fixes #402
2017-08-17 12:38:52 -07:00
Girish Ramakrishnan 78ec8e5c0c Add field to skip backup for an app
This skips the app from a backup when doing a full box backup and
simply reuses the previous backup.

The app can still be explicitly backed up using 'cloudron backup'
and explicitly restored using 'cloudron restore --backup'.

When restoring the box, it all depends on the app's last backup.

Fixes #311
2017-08-16 16:36:50 -07:00
Johannes Zellner 67a2ba957e Use maxsize logrotate rule instead of size
The current ruleset means rotate the file daily unless the file grows
larger than 1Mb earlier, then rotate once the file reaches that size.

https://serverfault.com/questions/474941/how-to-rotate-log-based-on-an-interval-unless-log-exceeds-a-certain-size
2017-08-16 19:10:49 +02:00
Girish Ramakrishnan 9e558924bb df plugin replaces with _ and not -
Part of #348
2017-08-15 09:32:42 -07:00
Johannes Zellner afcb3dd237 Fix layout issues in oauth views 2017-08-15 13:18:31 +02:00
Johannes Zellner 054de4813d Fix layout issue in update view 2017-08-15 11:04:47 +02:00
Girish Ramakrishnan 57891c64b5 use check_output instead
Aug 14 19:10:46 collectd[12651]: close failed in file object destructor:
Aug 14 19:10:46 collectd[12651]: IOError: [Errno 10] No child processes
2017-08-14 12:31:58 -07:00
Girish Ramakrishnan 26361c037d Merge branch 'mehdi/box-permissions'
Closes MR !14
2017-08-14 10:49:54 -07:00
Girish Ramakrishnan 2048b03431 Removed this file by mistake 2017-08-14 10:44:58 -07:00
Girish Ramakrishnan c12aba6c00 install xfsprogs
on some VPS like scaleway this is not installed.

This is why docker with devicemapper was using ext4 and not devmapper

devmapper: XFS is not supported in your system. Either the kernel doesn't support it or mkfs.xfs is not in your PATH. Defaulting to ext4 filesystem"
2017-08-13 23:15:23 -07:00
Girish Ramakrishnan 0bd0857189 Update many modules
npm WARN deprecated ejs-cli@1.2.0: This has breaking change. (in ejs package) use <= 2.0.0.
npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated graceful-fs@1.2.3: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.
2017-08-13 17:57:48 -07:00
Girish Ramakrishnan 978893250f Update superagent (for doubele callback bug) 2017-08-13 17:38:02 -07:00
mehdi d0f4a76ca2 basic capabilities syntax 2017-08-12 09:42:54 +01:00
72 changed files with 10685 additions and 4210 deletions
+43
View File
@@ -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
+2 -1
View File
@@ -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);
});
};
+1
View File
@@ -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
+559 -2808
View File
File diff suppressed because it is too large Load Diff
+26 -25
View File
@@ -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"
+3 -3
View File
@@ -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
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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');
+8
View File
@@ -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
};
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -18,7 +18,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.36.2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.36.3' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
+2 -3
View File
@@ -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
View File
@@ -4,7 +4,7 @@
rotate 7
daily
compress
size=1M
maxsize=1M
missingok
delaycompress
copytruncate
+46 -43
View File
@@ -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
View File
@@ -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 %>
+6 -4
View File
@@ -1,9 +1,11 @@
<footer class="text-center">
<span class="text-muted">&copy; 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">&copy; 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>
+8 -7
View File
@@ -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
View File
@@ -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>
+31 -26
View File
@@ -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 %>
+19 -16
View File
@@ -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 %>
+14 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
}
+4
View File
@@ -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));
});
}
+21
View File
@@ -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
};
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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'),
+46 -2
View File
@@ -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 () {
+1 -1
View File
@@ -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
View File
@@ -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
-1
View File
@@ -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';
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
});
+207
View File
@@ -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;
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+8 -2
View File
@@ -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 &amp; 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>
+41 -2
View File
@@ -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;
}]);
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">&copy;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">&copy;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>
+60 -61
View File
@@ -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 }} &nbsp;&nbsp;&nbsp;</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 }} &nbsp;&nbsp;&nbsp;</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>
+11 -4
View File
@@ -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;">&nbsp;</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
View File
@@ -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
View File
@@ -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">Lets 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 Lets 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">Lets 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 Lets 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>
+87
View File
@@ -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>
+339
View File
@@ -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
View File
@@ -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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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 -1
View File
@@ -1,4 +1,4 @@
<div class="content">
<div class="content content-large">
<div class="text-left">
<h2>Memory</h2>
-24
View File
@@ -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>
-81
View File
@@ -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
View File
@@ -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>
+72 -77
View File
@@ -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>
+76 -80
View File
@@ -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>
+96 -96
View File
@@ -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 }} &nbsp; <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 }} &nbsp; <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>