diff --git a/src/js/client.js b/src/js/client.js index 2926b18ee..830b9fe29 100644 --- a/src/js/client.js +++ b/src/js/client.js @@ -460,12 +460,12 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }); }; - Client.prototype.configureApp = function (id, data, callback) { - post('/api/v1/apps/' + id + '/configure', data, null, function (error, data, status) { + Client.prototype.configureApp = function (id, setting, data, callback) { + post('/api/v1/apps/' + id + '/configure/' + setting, data, null, function (error, data, status) { if (error) return callback(error); - if (status !== 202) return callback(new ClientError(status, data)); + if (status !== 200 && status !== 202) return callback(new ClientError(status, data)); - callback(null); + callback(null, data); }); }; @@ -1027,12 +1027,13 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.getApp = function (appId, callback) { + var that = this; + get('/api/v1/apps/' + appId, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); - var tmp = data.manifest.description.match(/\(.*)\<\/upstream\>/i); - data.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : ''; + that._appPostProcess(data); callback(null, data); }); @@ -1458,6 +1459,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout // amend the post install confirm state app.pendingPostInstallConfirmation = !!localStorage['confirmPostInstall_' + app.id]; + var tmp = app.manifest.description.match(/\(.*)\<\/upstream\>/i); + app.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : ''; + return app; }; diff --git a/src/js/index.js b/src/js/index.js index 532af0cc0..c2f1bc950 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -110,6 +110,9 @@ app.config(['$routeProvider', function ($routeProvider) { }).when('/users', { controller: 'UsersController', templateUrl: 'views/users.html?<%= revision %>' + }).when('/app/:appId', { + controller: 'AppController', + templateUrl: 'views/app.html?<%= revision %>' }).when('/appstore', { controller: 'AppStoreController', templateUrl: 'views/appstore.html?<%= revision %>' diff --git a/src/views/app.html b/src/views/app.html new file mode 100644 index 000000000..85daa1b02 --- /dev/null +++ b/src/views/app.html @@ -0,0 +1,415 @@ + + +
+ +
+

+ + {{ app.label || app.location || app.fqdn }} +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
App ID{{ app.id }}
Upstream Version{{ app.upstreamVersion }}
App Package{{ app.manifest.title }}
App Package Version{{ app.manifest.version }}
+
+ Documentation +
+
+
+ +

Display

+
+
+
+
+
+
+ +
{{display.error.label}}
+ +
+
+ + +
+
+
+ +
+
+
+
+ Reset Icon + +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Location

+
+
+
+
+
+
{{ location.error.other }}
+
+ +
+ + +
+ + +
+
+
+ +

+ Add an A record manually for {{ location.location }} to this Cloudron's public IP +
+

+ +
{{ location.error.port }}
+
+ +
+ + +
+
+
+ +
+ +
{{ location.error.alternateDomains }}
+ +
+
+
+ + +
+ + +
+
+
+
+ +
+
+
+ No alternate domains are configured. Add a domain +
+ +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Access Control

+
+
+
+
+
+
+
+ +

This setting also controls SFTP access.

+
+
+ +

+ This app has it's own user management. + This setting also controls SFTP access. +

+

+ This app is pre-configured for use with Cloudron Email. +

+
+ +
+ +
+
+ +
+
+
+
+ Users: +
+ +
+ Groups: +
+
+
+ + +
+
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Resources

+
+
+
+
+
+
+ +
+
+ +
+
+ + +
+ + +
{{resources.error.dataDir}}
+ +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Email

+
+
+
+
+
+ +
+ + +
{{ email.error.mailboxName }}
+ +
+ + +
+ +
+
+ +

+
+ This app is configured to send mail using {{app.domain}}'s Outbound Email settings. +

+
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Security

+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Updates

+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +

Backups

+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+ Saved +
+ +
+ +
+
+
+ +
diff --git a/src/views/app.js b/src/views/app.js new file mode 100644 index 000000000..6212f9bad --- /dev/null +++ b/src/views/app.js @@ -0,0 +1,464 @@ +'use strict'; + +/* global angular */ +/* global $ */ +/* global asyncSeries */ + +angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', 'Client', function ($scope, $location, $timeout, $interval, Client) { + Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); + + var appId = $location.path().slice('/app/'.length); + + $scope.app = null; + $scope.ready = false; + $scope.HOST_PORT_MIN = 1024; + $scope.HOST_PORT_MAX = 65535; + $scope.config = Client.getConfig(); + $scope.user = Client.getUserInfo(); + $scope.domains = []; + $scope.groups = []; + $scope.users = []; + $scope.backupsEnabled = true; + $scope.disableIndexingTemplate = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /'; + + $scope.display = { + busy: false, + error: {}, + success: false, + + tags: '', + label: '', + icon: { data: null }, + + iconUrl: function () { + if (!$scope.app) return ''; + + if ($scope.display.icon.data === '__original__') { // user clicked reset + return $scope.app.iconUrl + '&original=true'; + } else if ($scope.display.icon.data) { // user uploaded icon + return $scope.display.icon.data; + } else { // current icon + return $scope.app.iconUrl; + } + }, + + resetCustomIcon: function () { + $scope.display.icon.data = '__original__'; + }, + + showCustomIconSelector: function () { + $('#iconFileInput').click(); + }, + + show: function () { + var app = $scope.app; + + // translate for tag-input + $scope.display.tags = app.tags ? app.tags.join(',') : ''; + + $scope.display.label = $scope.app.label || ''; + $scope.display.icon = { data: null }; + + $('#iconFileInput').get(0).onchange = function (event) { + var fr = new FileReader(); + fr.onload = function () { + $scope.$apply(function () { + // var file = event.target.files[0]; + $scope.display.icon.data = fr.result; + }); + }; + fr.readAsDataURL(event.target.files[0]); + }; + }, + + submit: function () { + $scope.display.busy = true; + $scope.display.error = {}; + + // TODO break those apart + Client.configureApp($scope.app.id, 'label', { label: $scope.display.label }, function (error) { + if (error) return Client.error(error); + + var tags = $scope.display.tags.split(',').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; }); + + Client.configureApp($scope.app.id, 'tags', { tags: tags }, function (error) { + if (error) return Client.error(error); + + // skip if icon is unchanged + if ($scope.display.icon.data === null) { + $scope.display.busy = false; + $scope.display.success = true; + return; + } + + var icon; + if ($scope.display.icon.data === '__original__') { // user reset the icon + icon = ''; + } else if ($scope.display.icon.data) { // user loaded custom icon + icon = $scope.display.icon.data.replace(/^data:image\/[a-z]+;base64,/, ''); + } + + Client.configureApp($scope.app.id, 'icon', { icon: icon }, function (error) { + if (error) return Client.error(error); + + $scope.display.busy = false; + $scope.display.success = true; + }); + }); + }); + } + }; + + $scope.location = { + busy: false, + error: {}, + success: false, + + domain: null, + location: '', + alternateDomains: [], + portBindings: {}, + portBindingsEnabled: {}, + portBindingsInfo: {}, + + addAlternateDomain: function (event) { + event.preventDefault(); + $scope.location.alternateDomains.push({ + domain: $scope.domains[0], + subdomain: '' + }); + }, + + delAlternateDomain: function (event, index) { + event.preventDefault(); + $scope.location.alternateDomains.splice(index, 1); + }, + + show: function () { + var app = $scope.app; + + $scope.location.location = app.location; + $scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0]; + $scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information + $scope.location.alternateDomains = app.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };}); + + // fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port + for (var env in $scope.location.portBindingsInfo) { + if (app.portBindings && app.portBindings[env]) { + $scope.location.portBindings[env] = app.portBindings[env]; + $scope.location.portBindingsEnabled[env] = true; + } else { + $scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0; + $scope.location.portBindingsEnabled[env] = false; + } + } + }, + + submit: function () { + $scope.location.busy = true; + $scope.location.error = {}; + + // only use enabled ports from portBindings + var portBindings = {}; + for (var env in $scope.location.portBindings) { + if ($scope.location.portBindingsEnabled[env]) { + portBindings[env] = $scope.location.portBindings[env]; + } + } + + var data = { + location: $scope.location.location, + domain: $scope.location.domain.domain, + portBindings: portBindings, + alternateDomains: $scope.location.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };}) + }; + + Client.configureApp($scope.app.id, 'location', data, function (error) { + if (error) return Client.error(error); + + $scope.location.success = true; + $scope.location.busy = false; + }); + } + }; + + $scope.access = { + busy: false, + error: {}, + success: false, + + ftp: false, + ssoAuth: false, + accessRestrictionOption: 'any', + accessRestriction: { users: [], groups: [] }, + + isAccessRestrictionValid: function () { + var tmp = $scope.access.accessRestriction; + return !!(tmp.users.length || tmp.groups.length); + }, + + show: function () { + var app = $scope.app; + + $scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp; + $scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso; + $scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any'; + $scope.access.accessRestriction = { users: [], groups: [] }; + + if (app.accessRestriction) { + var userSet = { }; + app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; }); + $scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); }); + + var groupSet = { }; + app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; }); + $scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); }); + } + }, + + submit: function () { + $scope.access.busy = true; + $scope.access.error = {}; + + var accessRestriction = null; + if ($scope.access.accessRestrictionOption === 'groups') { + accessRestriction = { users: [], groups: [] }; + accessRestriction.users = $scope.access.accessRestriction.users.map(function (u) { return u.id; }); + accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; }); + } + + Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, function (error) { + if (error) return Client.error(error); + + $scope.access.success = true; + $scope.access.busy = false; + }); + } + }; + + $scope.resources = { + busy: false, + error: {}, + success: false, + + memoryLimit: 0, + memoryTicks: [], + dataDir: null, + dataDirEnabled: false, + + show: function () { + var app = $scope.app; + + $scope.resources.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024); + $scope.resources.dataDirEnabled = !!app.dataDir; + $scope.resources.dataDir = app.dataDir; + + // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below) + // TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates + $scope.resources.memoryTicks = []; + var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2))); + for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) { + if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024); + } + if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) { + $scope.resources.memoryTicks.unshift(app.manifest.memoryLimit); + } + }, + + submit: function () { + $scope.resources.busy = true; + $scope.resources.error = {}; + + var memoryLimit = $scope.resources.memoryLimit === $scope.resources.memoryTicks[0] ? 0 : $scope.resources.memoryLimit; + + Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) { + if (error) return Client.error(error); + + $scope.resources.success = true; + $scope.resources.busy = false; + + // TODO handle data dir once we show it + }); + } + }; + + $scope.email = { + busy: false, + error: {}, + success: false, + + mailboxNameEnabled: false, + mailboxName: '', + domain: '', + + show: function () { + var app = $scope.app; + + $scope.email.mailboxNameEnabled = app.mailboxName && (app.mailboxName.match(/\.app$/) === null); + $scope.email.mailboxName = app.mailboxName || ''; + $scope.email.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0]; + }, + + submit: function () { + $scope.email.busy = true; + } + }; + + $scope.security = { + busy: false, + error: {}, + success: false, + + robotsTxt: '', + + show: function () { + var app = $scope.app; + + $scope.security.robotsTxt = app.robotsTxt; + }, + + submit: function () { + $scope.security.busy = true; + $scope.security.error = {}; + + Client.configureApp($scope.app.id, 'robots_txt', { robotsTxt: $scope.security.robotsTxt }, function (error) { + if (error) return Client.error(error); + + $scope.security.success = true; + $scope.security.busy = false; + }); + } + }; + + $scope.updates = { + busy: false, + error: {}, + success: false, + + enableAutomaticUpdate: false, + + show: function () { + var app = $scope.app; + + $scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate; + }, + + submit: function () { + $scope.updates.busy = true; + $scope.updates.error = {}; + + Client.configureApp($scope.app.id, 'automatic_update', { enable: $scope.updates.enableAutomaticUpdate }, function (error) { + if (error) return Client.error(error); + + $scope.updates.success = true; + $scope.updates.busy = false; + }); + } + }; + + $scope.backups = { + busy: false, + error: {}, + success: false, + + enableBackup: false, + + show: function () { + var app = $scope.app; + + $scope.backups.enableBackup = app.enableBackup; + }, + + submit: function () { + $scope.backups.busy = true; + $scope.backups.error = {}; + + Client.configureApp($scope.app.id, 'automatic_backup', { enable: $scope.backups.enableBackup }, function (error) { + if (error) return Client.error(error); + + $scope.backups.success = true; + $scope.backups.busy = false; + }); + } + }; + + function fetchUsers(callback) { + Client.getUsers(function (error, users) { + if (error) return callback(error); + + // ensure we have something to work with in the access restriction dropdowns + users.forEach(function (user) { user.display = user.username || user.email; }); + + $scope.users = users; + + callback(); + }); + } + + function fetchGroups(callback) { + Client.getGroups(function (error, groups) { + if (error) return callback(error); + + $scope.groups = groups; + + callback(); + }); + } + + function getDomains(callback) { + Client.getDomains(function (error, result) { + if (error) return callback(error); + + $scope.domains = result; + + callback(); + }); + } + + function getBackupConfig(callback) { + Client.getBackupConfig(function (error, backupConfig) { + if (error) return callback(error); + + $scope.backupEnabled = backupConfig.provider !== 'noop'; + + callback(); + }); + } + + Client.onReady(function () { + $scope.app = Client.getApp(appId, function (error, app) { + if (error) return Client.error(error); + + $scope.app = app; + + asyncSeries([ + fetchUsers, + fetchGroups, + getDomains, + getBackupConfig + ], function (error) { + if (error) return Client.error(error); + + $scope.display.show(); + $scope.location.show(); + $scope.resources.show(); + $scope.access.show(); + $scope.email.show(); + $scope.security.show(); + $scope.backups.show(); + $scope.updates.show(); + + $scope.ready = true; + }); + }); + }); + + // setup all the dialog focus handling + ['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal', 'appInfoModal', 'appErrorModal'].forEach(function (id) { + $('#' + id).on('shown.bs.modal', function () { + $(this).find("[autofocus]:first").focus(); + }); + }); + + $('.modal-backdrop').remove(); +}]); diff --git a/src/views/apps.html b/src/views/apps.html index f0c2c3907..d310c805c 100644 --- a/src/views/apps.html +++ b/src/views/apps.html @@ -608,7 +608,7 @@ - + diff --git a/src/views/apps.js b/src/views/apps.js index c5df2cf95..601e8daf1 100644 --- a/src/views/apps.js +++ b/src/views/apps.js @@ -36,14 +36,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location certificateFileName: '', keyFile: null, keyFileName: '', - memoryLimit: 0, - memoryTicks: [], mailboxName: '', accessRestrictionOption: 'any', accessRestriction: { users: [], groups: [] }, - dataDir: null, alternateDomains: [], mailboxNameEnabled: false, + memoryLimit: 0, + memoryTicks: [], + dataDir: null, dataDirEnabled: false, ssoAuth: false, ftp: false, @@ -100,7 +100,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location $scope.appConfigure.mailboxName = app.mailboxName || ''; $scope.appConfigure.label = app.label || ''; - $scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso; $scope.appConfigure.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp; // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below) @@ -114,6 +113,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location $scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit); } + $scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso; $scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any'; $scope.appConfigure.accessRestriction = { users: [], groups: [] };