598 lines
24 KiB
JavaScript
598 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', 'AppStore', function ($scope, $location, $timeout, $routeParams, Client, AppStore) {
|
|
Client.onReady(function () { if (!Client.getUserInfo().admin && !Client.getConfig().features.spaces) $location.path('/'); });
|
|
|
|
$scope.HOST_PORT_MIN = 1024;
|
|
$scope.HOST_PORT_MAX = 65535;
|
|
|
|
$scope.ready = false;
|
|
$scope.apps = [];
|
|
$scope.config = Client.getConfig();
|
|
$scope.user = Client.getUserInfo();
|
|
$scope.users = [];
|
|
$scope.groups = [];
|
|
$scope.domains = [];
|
|
$scope.category = '';
|
|
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
|
$scope.searchString = '';
|
|
$scope.validAppstoreAccount = false;
|
|
$scope.appstoreConfig = null;
|
|
$scope.spacesSuffix = '';
|
|
|
|
$scope.showView = function (view) {
|
|
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
|
$('.modal').on('hidden.bs.modal', function () {
|
|
$scope.$apply(function () {
|
|
$scope.appInstall.reset();
|
|
$('.modal').off('hidden.bs.modal');
|
|
$location.path(view);
|
|
});
|
|
});
|
|
|
|
$('.modal').modal('hide');
|
|
};
|
|
|
|
$scope.appInstall = {
|
|
busy: false,
|
|
state: 'appInfo',
|
|
error: {},
|
|
app: {},
|
|
location: '',
|
|
domain: null,
|
|
portBindings: {},
|
|
mediaLinks: [],
|
|
certificateFile: null,
|
|
certificateFileName: '',
|
|
keyFile: null,
|
|
keyFileName: '',
|
|
accessRestrictionOption: 'any',
|
|
accessRestriction: { users: [], groups: [] },
|
|
customAuth: false,
|
|
optionalSso: false,
|
|
subscriptionErrorMesssage: '',
|
|
|
|
isAccessRestrictionValid: function () {
|
|
var tmp = $scope.appInstall.accessRestriction;
|
|
return !!(tmp.users.length || tmp.groups.length);
|
|
},
|
|
|
|
reset: function () {
|
|
$scope.appInstall.app = {};
|
|
$scope.appInstall.error = {};
|
|
$scope.appInstall.location = '';
|
|
$scope.appInstall.domain = null;
|
|
$scope.appInstall.portBindings = {};
|
|
$scope.appInstall.state = 'appInfo';
|
|
$scope.appInstall.mediaLinks = [];
|
|
$scope.appInstall.certificateFile = null;
|
|
$scope.appInstall.certificateFileName = '';
|
|
$scope.appInstall.keyFile = null;
|
|
$scope.appInstall.keyFileName = '';
|
|
$scope.appInstall.accessRestrictionOption = 'any';
|
|
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
|
$scope.appInstall.optionalSso = false;
|
|
$scope.appInstall.customAuth = false;
|
|
$scope.appInstall.subscriptionErrorMesssage = '';
|
|
|
|
$('#collapseInstallForm').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('hide');
|
|
$('#collapseAppLimitReached').collapse('hide');
|
|
$('#collapseMediaLinksCarousel').collapse('show');
|
|
|
|
if ($scope.appInstallForm) {
|
|
$scope.appInstallForm.$setPristine();
|
|
$scope.appInstallForm.$setUntouched();
|
|
}
|
|
},
|
|
|
|
showForm: function (force) {
|
|
if (Client.enoughResourcesAvailable($scope.appInstall.app) || force) {
|
|
$scope.appInstall.state = 'installForm';
|
|
$('#collapseMediaLinksCarousel').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('hide');
|
|
$('#collapseInstallForm').collapse('show');
|
|
$('#appInstallLocationInput').focus();
|
|
} else {
|
|
$scope.appInstall.state = 'resourceConstraint';
|
|
$('#collapseMediaLinksCarousel').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('show');
|
|
}
|
|
},
|
|
|
|
show: function (app) { // this is an appstore app object!
|
|
$scope.appInstall.reset();
|
|
|
|
// make a copy to work with in case the app object gets updated while polling
|
|
angular.copy(app, $scope.appInstall.app);
|
|
|
|
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
|
$scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
|
|
$scope.appInstall.portBindingsInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
|
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
|
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
|
|
|
var manifest = app.manifest;
|
|
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
|
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oauth']);
|
|
|
|
// for spaces users, the User management is hidden. thus the admin flag check
|
|
if (!$scope.user.admin) {
|
|
// just install it with access restriction as just the user
|
|
var me = $scope.users.find(function (u) { return u.id === $scope.user.id; });
|
|
$scope.appInstall.accessRestrictionOption = 'groups';
|
|
$scope.appInstall.accessRestriction = { users: [ me ], groups: [] };
|
|
} else {
|
|
$scope.appInstall.accessRestrictionOption = 'any';
|
|
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
|
}
|
|
|
|
// set default ports
|
|
var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
|
|
for (var env in allPorts) {
|
|
$scope.appInstall.portBindings[env] = allPorts[env].defaultValue || 0;
|
|
$scope.appInstall.portBindingsEnabled[env] = true;
|
|
}
|
|
|
|
$('#appInstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.appInstall.busy = true;
|
|
$scope.appInstall.error.other = null;
|
|
$scope.appInstall.error.location = null;
|
|
$scope.appInstall.error.port = null;
|
|
|
|
// only use enabled ports from portBindings
|
|
var finalPortBindings = {};
|
|
for (var env in $scope.appInstall.portBindings) {
|
|
if ($scope.appInstall.portBindingsEnabled[env]) {
|
|
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
|
}
|
|
}
|
|
|
|
var finalAccessRestriction = null;
|
|
if ($scope.appInstall.accessRestrictionOption === 'groups') {
|
|
finalAccessRestriction = { users: [], groups: [] };
|
|
finalAccessRestriction.users = $scope.appInstall.accessRestriction.users.map(function (u) { return u.id; });
|
|
finalAccessRestriction.groups = $scope.appInstall.accessRestriction.groups.map(function (g) { return g.id; });
|
|
}
|
|
|
|
var data = {
|
|
location: $scope.appInstall.location || '',
|
|
domain: $scope.appInstall.domain.domain,
|
|
portBindings: finalPortBindings,
|
|
accessRestriction: finalAccessRestriction,
|
|
cert: $scope.appInstall.certificateFile,
|
|
key: $scope.appInstall.keyFile,
|
|
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
|
|
};
|
|
|
|
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error, newAppId) {
|
|
if (error) {
|
|
if (error.statusCode === 402) {
|
|
$scope.appInstall.state = 'appLimitReached';
|
|
$scope.appInstall.subscriptionErrorMesssage = error.message;
|
|
$('#collapseMediaLinksCarousel').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('hide');
|
|
$('#collapseInstallForm').collapse('hide');
|
|
$('#collapseAppLimitReached').collapse('show');
|
|
} else if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
|
$scope.appInstall.error.port = error.message;
|
|
} else if (error.statusCode === 409) {
|
|
$scope.appInstall.error.location = 'This name is already taken.';
|
|
$scope.appInstallForm.location.$setPristine();
|
|
$('#appInstallLocationInput').focus();
|
|
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
|
$scope.appInstall.error.cert = error.message;
|
|
$scope.appInstall.certificateFileName = '';
|
|
$scope.appInstall.certificateFile = null;
|
|
$scope.appInstall.keyFileName = '';
|
|
$scope.appInstall.keyFile = null;
|
|
} else {
|
|
$scope.appInstall.error.other = error.message;
|
|
}
|
|
|
|
$scope.appInstall.busy = false;
|
|
return;
|
|
}
|
|
|
|
$scope.appInstall.busy = false;
|
|
|
|
// stash new app id for later
|
|
$scope.appInstall.app.id = newAppId;
|
|
|
|
// we track the postinstall confirmation for the current user's browser
|
|
// TODO later we might want to have a notification db to track the state across admins and browsers
|
|
if ($scope.appInstall.app.manifest.postInstallMessage) {
|
|
localStorage['confirmPostInstall_' + $scope.appInstall.app.id] = true;
|
|
}
|
|
|
|
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
|
$('#appInstallModal').on('hidden.bs.modal', function () {
|
|
$scope.$apply(function () {
|
|
$location.path('/apps').search({ });
|
|
});
|
|
});
|
|
|
|
$('#appInstallModal').modal('hide');
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.appNotFound = {
|
|
appId: '',
|
|
version: ''
|
|
};
|
|
|
|
$scope.appstoreLogin = {
|
|
busy: false,
|
|
error: {},
|
|
email: '',
|
|
password: '',
|
|
totpToken: '',
|
|
register: true,
|
|
termsAccepted: false,
|
|
|
|
submit: function () {
|
|
$scope.appstoreLogin.error = {};
|
|
$scope.appstoreLogin.busy = true;
|
|
|
|
function login() {
|
|
AppStore.login($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, function (error, result) {
|
|
if (error) {
|
|
$scope.appstoreLogin.busy = false;
|
|
|
|
if (error.statusCode === 401) {
|
|
if (error.message.indexOf('TOTP token missing') !== -1) {
|
|
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
|
|
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
|
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
|
|
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
|
|
$scope.appstoreLogin.totpToken = '';
|
|
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
|
} else {
|
|
$scope.appstoreLogin.error.password = 'Wrong email or password';
|
|
$scope.appstoreLogin.password = '';
|
|
$('#inputAppstoreLoginPassword').focus();
|
|
$scope.appstoreLoginForm.password.$setPristine();
|
|
}
|
|
} else {
|
|
console.error(error);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var config = {
|
|
userId: result.userId,
|
|
token: result.accessToken
|
|
};
|
|
|
|
Client.setAppstoreConfig(config, function (error) {
|
|
if (error) {
|
|
$scope.appstoreLogin.busy = false;
|
|
|
|
if (error.statusCode === 424) {
|
|
if (error.message === 'wrong user') {
|
|
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
|
|
$scope.appstoreLogin.email = '';
|
|
$scope.appstoreLogin.password = '';
|
|
$scope.appstoreLoginForm.email.$setPristine();
|
|
$scope.appstoreLoginForm.password.$setPristine();
|
|
$('#inputAppstoreLoginEmail').focus();
|
|
} else {
|
|
console.error(error);
|
|
$scope.appstoreLogin.error.generic = 'Please retry later';
|
|
}
|
|
} else {
|
|
console.error(error);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
fetchAppstoreConfig(function (error) {
|
|
if (error) return console.error('Unable to fetch appstore config.', error);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!$scope.appstoreLogin.register) return login();
|
|
|
|
AppStore.register($scope.appstoreLogin.email, $scope.appstoreLogin.password, function (error) {
|
|
if (error) {
|
|
$scope.appstoreLogin.busy = false;
|
|
|
|
if (error.statusCode === 409) {
|
|
$scope.appstoreLogin.error.email = 'An account with this email already exists';
|
|
$scope.appstoreLogin.password = '';
|
|
$scope.appstoreLoginForm.email.$setPristine();
|
|
$scope.appstoreLoginForm.password.$setPristine();
|
|
$('#inputAppstoreLoginEmail').focus();
|
|
} else {
|
|
console.error(error);
|
|
$scope.appstoreLogin.error.generic = 'Please retry later';
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
login();
|
|
});
|
|
}
|
|
};
|
|
|
|
function getAppList(callback) {
|
|
AppStore.getApps(function (error, apps) {
|
|
if (error) return callback(error);
|
|
|
|
// ensure we have a tags property for further use
|
|
apps.forEach(function (app) {
|
|
if (!app.manifest.tags) app.manifest.tags = [];
|
|
});
|
|
|
|
return callback(null, apps);
|
|
});
|
|
}
|
|
|
|
// TODO does not support testing apps in search
|
|
$scope.search = function () {
|
|
if (!$scope.searchString) return $scope.showCategory(null, $scope.cachedCategory);
|
|
|
|
$scope.category = '';
|
|
|
|
AppStore.getAppsFast(function (error, apps) {
|
|
if (error) return $timeout($scope.search, 1000);
|
|
|
|
var token = $scope.searchString.toUpperCase();
|
|
|
|
$scope.apps = apps.filter(function (app) {
|
|
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
|
|
return false;
|
|
});
|
|
});
|
|
};
|
|
|
|
$scope.showCategory = function (event, category) {
|
|
if (!event) $scope.category = category;
|
|
else $scope.category = event.target.getAttribute('category');
|
|
|
|
$scope.cachedCategory = $scope.category;
|
|
|
|
AppStore.getAppsFast(function (error, apps) {
|
|
if (error) return $timeout($scope.showCategory.bind(null, event), 1000);
|
|
|
|
if (!$scope.category) {
|
|
$scope.apps = apps;
|
|
} else if ($scope.category === 'featured') {
|
|
$scope.apps = apps.filter(function (app) { return app.featured; });
|
|
} else {
|
|
$scope.apps = apps.filter(function (app) {
|
|
return app.manifest.tags.some(function (tag) { return $scope.category === tag; });
|
|
});
|
|
}
|
|
|
|
document.getElementById('appstoreGrid').scrollIntoView();
|
|
});
|
|
};
|
|
|
|
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
|
|
$scope.$apply(function () {
|
|
$scope.appInstall.certificateFile = null;
|
|
$scope.appInstall.certificateFileName = event.target.files[0].name;
|
|
|
|
var reader = new FileReader();
|
|
reader.onload = function (result) {
|
|
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
|
$scope.appInstall.certificateFile = result.target.result;
|
|
};
|
|
reader.readAsText(event.target.files[0]);
|
|
});
|
|
};
|
|
|
|
document.getElementById('appInstallKeyFileInput').onchange = function (event) {
|
|
$scope.$apply(function () {
|
|
$scope.appInstall.keyFile = null;
|
|
$scope.appInstall.keyFileName = event.target.files[0].name;
|
|
|
|
var reader = new FileReader();
|
|
reader.onload = function (result) {
|
|
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
|
$scope.appInstall.keyFile = result.target.result;
|
|
};
|
|
reader.readAsText(event.target.files[0]);
|
|
});
|
|
};
|
|
|
|
$scope.showAppNotFound = function (appId, version) {
|
|
$scope.appNotFound.appId = appId;
|
|
$scope.appNotFound.version = version;
|
|
|
|
$('#appNotFoundModal').modal('show');
|
|
};
|
|
|
|
$scope.gotoApp = function (app) {
|
|
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
|
};
|
|
|
|
function hashChangeListener() {
|
|
// event listener is called from DOM not angular, need to use $apply
|
|
$scope.$apply(function () {
|
|
var appId = $location.path().slice('/appstore/'.length);
|
|
var version = $location.search().version;
|
|
|
|
if (appId) {
|
|
if (version) {
|
|
AppStore.getAppByIdAndVersion(appId, version, function (error, result) {
|
|
if (error) {
|
|
$scope.showAppNotFound(appId, version);
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
$scope.appInstall.show(result);
|
|
});
|
|
} else {
|
|
AppStore.getAppById(appId, function (error, result) {
|
|
if (error) {
|
|
$scope.showAppNotFound(appId, null);
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
$scope.appInstall.show(result);
|
|
});
|
|
}
|
|
} else {
|
|
$scope.appInstall.reset();
|
|
}
|
|
});
|
|
}
|
|
|
|
function fetchUsers() {
|
|
Client.getUsers(function (error, users) {
|
|
if (error) {
|
|
console.error(error);
|
|
return $timeout(fetchUsers, 5000);
|
|
}
|
|
|
|
$scope.users = users;
|
|
});
|
|
}
|
|
|
|
function fetchGroups() {
|
|
Client.getGroups(function (error, groups) {
|
|
if (error) {
|
|
console.error(error);
|
|
return $timeout(fetchUsers, 5000);
|
|
}
|
|
|
|
$scope.groups = groups;
|
|
});
|
|
}
|
|
|
|
function fetchAppstoreConfig(callback) {
|
|
callback = callback || function (error) { if (error) console.error(error); };
|
|
|
|
// non admins cannot read appstore settings in spaces mode
|
|
if (!$scope.user.admin && $scope.config.features.spaces) {
|
|
$scope.validAppstoreAccount = true;
|
|
return callback();
|
|
}
|
|
|
|
if ($scope.user.admin && $scope.config.managed) {
|
|
$scope.validAppstoreAccount = true;
|
|
return callback();
|
|
}
|
|
|
|
Client.getAppstoreConfig(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
if (!result.token || !result.cloudronId) return callback();
|
|
|
|
var appstoreConfig = result;
|
|
|
|
AppStore.getCloudronDetails(appstoreConfig, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
AppStore.getProfile(appstoreConfig.token, function (error, result) {
|
|
if (error) return console.error(error);
|
|
|
|
// assign late to avoid UI flicketing on update
|
|
appstoreConfig.profile = result;
|
|
$scope.appstoreConfig = appstoreConfig;
|
|
|
|
$scope.validAppstoreAccount = true;
|
|
|
|
// clear busy state when a login/signup was performed
|
|
$scope.appstoreLogin.busy = false;
|
|
|
|
// also update the root controller status
|
|
if ($scope.$parent) {
|
|
$scope.$parent.fetchAppstoreProfileAndSubscription(function (error) {
|
|
if (error) console.error(error);
|
|
});
|
|
}
|
|
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
$scope.ready = false;
|
|
|
|
$scope.spacesSuffix = $scope.user.username.replace(/\./g, '-');
|
|
|
|
getAppList(function (error, apps) {
|
|
if (error) {
|
|
console.error(error);
|
|
return $timeout(init, 1000);
|
|
}
|
|
|
|
$scope.apps = apps;
|
|
|
|
fetchUsers();
|
|
fetchGroups();
|
|
|
|
// start with featured apps listing
|
|
$scope.showCategory(null, 'featured');
|
|
|
|
// domains is required since we populate the dropdown with domains[0]
|
|
Client.getDomains(function (error, result) {
|
|
if (error) console.error(error);
|
|
|
|
$scope.domains = result;
|
|
|
|
// show install app dialog immediately if an app id was passed in the query
|
|
// hashChangeListener calls $apply, so make sure we don't double digest here
|
|
setTimeout(hashChangeListener, 1);
|
|
|
|
fetchAppstoreConfig(function (error) {
|
|
if (error) console.error(error);
|
|
$scope.ready = true;
|
|
|
|
setTimeout(function () { $('#appstoreSearch').focus(); }, 1000);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Client.onReady(init);
|
|
|
|
// note: do not use hide.bs.model since it is called immediately from switchToAppsView which is already in angular scope
|
|
$('#appInstallModal').on('hidden.bs.modal', function () {
|
|
// clear the appid and version in the search bar when dialog is cancelled
|
|
$scope.$apply(function () {
|
|
$location.path('/appstore', false).search({ }); // 'false' means do not reload
|
|
});
|
|
});
|
|
|
|
window.addEventListener('hashchange', hashChangeListener);
|
|
|
|
$scope.$on('$destroy', function handler() {
|
|
window.removeEventListener('hashchange', hashChangeListener);
|
|
});
|
|
|
|
// setup all the dialog focus handling
|
|
['appInstallModal'].forEach(function (id) {
|
|
$('#' + id).on('shown.bs.modal', function () {
|
|
$(this).find("[autofocus]:first").focus();
|
|
});
|
|
});
|
|
|
|
// autofocus if appstore login is shown
|
|
$scope.$watch('validAppstoreAccount', function (newValue, oldValue) {
|
|
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
|
});
|
|
|
|
$('.modal-backdrop').remove();
|
|
}]);
|