'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(); }]);