788 lines
33 KiB
JavaScript
788 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular */
|
|
/* global $ */
|
|
/* global async */
|
|
/* global ERROR */
|
|
/* global RSTATES */
|
|
/* global moment */
|
|
|
|
angular.module('Application').controller('AppStoreController', ['$scope', '$translate', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $translate, $location, $timeout, $routeParams, Client) {
|
|
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
|
|
|
$scope.HOST_PORT_MIN = 1;
|
|
$scope.HOST_PORT_MAX = 65535;
|
|
|
|
$scope.ready = false;
|
|
$scope.apps = [];
|
|
$scope.popularApps = [];
|
|
$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.validSubscription = false;
|
|
$scope.subscription = {};
|
|
$scope.memory = null; // { memory, swap }
|
|
|
|
$scope.showView = function (view) {
|
|
$('#appInstallModal').off('hidden.bs.modal');
|
|
|
|
// 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.appInstall.reset();
|
|
$('.modal').off('hidden.bs.modal');
|
|
$location.path(view).search({});
|
|
});
|
|
|
|
$('.modal').modal('hide');
|
|
};
|
|
|
|
// If new categories added make sure the translation below exists
|
|
$scope.categories = [
|
|
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
|
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
|
|
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
|
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
|
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
|
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
|
|
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
|
|
{ id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
|
|
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
|
|
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
|
|
{ id: 'fun', icon: 'fa fa-gamepad', label: 'Fun'},
|
|
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
|
|
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
|
|
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
|
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
|
{ id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
|
|
{ id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
|
|
{ id: 'no-code', icon: 'fas fa-code', label: 'No-code'},
|
|
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
|
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
|
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
|
{ id: 'voip', icon: 'fa fa-headset', label: 'VoIP'},
|
|
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
|
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
|
];
|
|
|
|
// Translation IDs are generated as "appstore.category.<categoryId>"
|
|
$translate($scope.categories.map(function (c) { return 'appstore.category.' + c.id; })).then(function (tr) {
|
|
Object.keys(tr).forEach(function (key) {
|
|
if (key === tr[key]) return; // missing translation use default label
|
|
|
|
var category = $scope.categories.find(function (c) { return key.endsWith(c.id); });
|
|
if (category) category.label = tr[key];
|
|
});
|
|
});
|
|
|
|
$scope.categoryButtonLabel = function (category) {
|
|
var categoryLabel = $translate.instant('appstore.categoryLabel');
|
|
|
|
if (category === '') return $translate.instant('appstore.category.all');
|
|
if (category === 'new') return $translate.instant('appstore.category.newApps');
|
|
|
|
var tmp = $scope.categories.find(function (c) { return c.id === category; });
|
|
if (tmp) return tmp.label;
|
|
|
|
return categoryLabel;
|
|
};
|
|
|
|
$scope.isProxyApp = function (app) {
|
|
if (!app) return false;
|
|
|
|
return app.id === 'io.cloudron.builtin.appproxy';
|
|
};
|
|
|
|
$scope.userManagementFilterOptions = [
|
|
{ id: '', icon: '', label: $translate.instant('appstore.ssofilter.all') },
|
|
{ id: 'sso', icon: 'fas fa-user', label: $translate.instant('apps.auth.sso') },
|
|
{ id: 'nosso', icon: 'far fa-user', label: $translate.instant('apps.auth.nosso') },
|
|
{ id: 'email', icon: 'fas fa-envelope', label: $translate.instant('apps.auth.email') },
|
|
];
|
|
$scope.userManagementFilterOption = $scope.userManagementFilterOptions[0];
|
|
|
|
$scope.userManagementFilterOptionIsActive = function (option) {
|
|
return option.id === $scope.userManagementFilterOption.id;
|
|
};
|
|
|
|
$scope.applyUserMangamentFilter = function (option) {
|
|
$scope.userManagementFilterOption = option;
|
|
};
|
|
|
|
$scope.appInstall = {
|
|
busy: false,
|
|
state: 'appInfo',
|
|
error: {},
|
|
app: {},
|
|
needsOverwrite: false,
|
|
subdomain: '',
|
|
domain: null, // object and not the string
|
|
secondaryDomains: {},
|
|
ports: {},
|
|
portsEnabled: {},
|
|
mediaLinks: [],
|
|
keyFile: null,
|
|
keyFileName: '',
|
|
accessRestrictionOption: '',
|
|
accessRestriction: { users: [], groups: [] },
|
|
customAuth: false,
|
|
optionalSso: false,
|
|
subscriptionErrorMesssage: '',
|
|
upstreamUri: '',
|
|
|
|
isAccessRestrictionValid: function () {
|
|
var tmp = $scope.appInstall.accessRestriction;
|
|
return !!(tmp.users.length || tmp.groups.length);
|
|
},
|
|
|
|
reset: function () {
|
|
$scope.appInstall.app = {};
|
|
$scope.appInstall.error = {};
|
|
$scope.appInstall.needsOverwrite = false;
|
|
$scope.appInstall.subdomain = '';
|
|
$scope.appInstall.domain = null;
|
|
$scope.appInstall.secondaryDomains = {};
|
|
$scope.appInstall.ports = {};
|
|
$scope.appInstall.state = 'appInfo';
|
|
$scope.appInstall.mediaLinks = [];
|
|
$scope.appInstall.keyFile = null;
|
|
$scope.appInstall.keyFileName = '';
|
|
$scope.appInstall.accessRestrictionOption = '';
|
|
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
|
$scope.appInstall.optionalSso = false;
|
|
$scope.appInstall.customAuth = false;
|
|
$scope.appInstall.subscriptionErrorMesssage = '';
|
|
$scope.appInstall.upstreamUri = '';
|
|
|
|
$('#collapseInstallForm').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('hide');
|
|
$('#collapseSubscriptionRequired').collapse('hide');
|
|
$('#collapseMediaLinksCarousel').collapse('show');
|
|
|
|
if ($scope.appInstallForm) {
|
|
$scope.appInstallForm.$setPristine();
|
|
$scope.appInstallForm.$setUntouched();
|
|
}
|
|
},
|
|
|
|
showForm: function (force) {
|
|
var app = $scope.appInstall.app;
|
|
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
|
|
|
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM
|
|
var used = Client.getInstalledApps().reduce(function (prev, cur) {
|
|
if (cur.runState === RSTATES.STOPPED) return prev;
|
|
|
|
return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
|
|
}, 0);
|
|
var totalMemory = $scope.memory.memory * 2;
|
|
var available = (totalMemory || 0) - used;
|
|
|
|
var enoughResourcesAvailable = (available - needed) >= 0;
|
|
|
|
if (enoughResourcesAvailable || 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.secondaryDomains = {};
|
|
var httpPorts = $scope.appInstall.app.manifest.httpPorts || {};
|
|
for (var env2 in httpPorts) {
|
|
$scope.appInstall.secondaryDomains[env2] = {
|
|
subdomain: httpPorts[env2].defaultValue || '',
|
|
domain: $scope.appInstall.domain
|
|
};
|
|
}
|
|
|
|
$scope.appInstall.portInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
|
$scope.appInstall.ports = {}; // This holds the env:port pair
|
|
$scope.appInstall.portsEnabled = {}; // This holds the enabled/disabled flag
|
|
|
|
var manifest = app.manifest;
|
|
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
|
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oidc'] || manifest.addons['proxyAuth']);
|
|
|
|
$scope.appInstall.accessRestrictionOption = $scope.groups.length ? '' : 'any'; // make the user select an ACL conciously if groups are used
|
|
$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.ports[env] = allPorts[env].defaultValue || 0;
|
|
$scope.appInstall.portsEnabled[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;
|
|
|
|
var secondaryDomains = {};
|
|
for (var env2 in $scope.appInstall.secondaryDomains) {
|
|
secondaryDomains[env2] = {
|
|
subdomain: $scope.appInstall.secondaryDomains[env2].subdomain,
|
|
domain: $scope.appInstall.secondaryDomains[env2].domain.domain
|
|
};
|
|
}
|
|
|
|
// only use enabled ports from ports
|
|
var finalPorts = {};
|
|
for (var env in $scope.appInstall.ports) {
|
|
if ($scope.appInstall.portsEnabled[env]) {
|
|
finalPorts[env] = $scope.appInstall.ports[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 = {
|
|
overwriteDns: $scope.appInstall.needsOverwrite,
|
|
subdomain: $scope.appInstall.subdomain || '',
|
|
domain: $scope.appInstall.domain.domain,
|
|
secondaryDomains: secondaryDomains,
|
|
ports: finalPorts,
|
|
accessRestriction: finalAccessRestriction,
|
|
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso'),
|
|
};
|
|
|
|
if ($scope.appInstall.upstreamUri) {
|
|
data.upstreamUri = $scope.appInstall.upstreamUri;
|
|
data.upstreamUri = data.upstreamUri.replace(/\/$/, '');
|
|
}
|
|
|
|
var domains = [];
|
|
domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
|
|
var canInstall = true;
|
|
|
|
async.eachSeries(domains, function (domain, callback) {
|
|
if (data.overwriteDns) return callback();
|
|
|
|
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
var message;
|
|
if (result.error) {
|
|
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
|
message = 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view';
|
|
if (domain.type === 'primary') {
|
|
$scope.appInstall.error.location = message;
|
|
} else {
|
|
$scope.appInstall.error.secondaryDomain = message;
|
|
}
|
|
} else {
|
|
if (domain.type === 'primary') {
|
|
$scope.appInstall.error.location = result.error.message;
|
|
} else {
|
|
$scope.appInstall.error.secondaryDomain = message;
|
|
}
|
|
}
|
|
canInstall = false;
|
|
} else if (result.needsOverwrite) {
|
|
message = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
|
if (data.type === 'primary') {
|
|
$scope.appInstall.error.location = message;
|
|
} else {
|
|
$scope.appInstall.error.secondaryDomain = message;
|
|
}
|
|
$scope.appInstall.needsOverwrite = true;
|
|
canInstall = false;
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}, function (error) {
|
|
if (error) {
|
|
$scope.appInstall.busy = false;
|
|
return Client.error(error);
|
|
}
|
|
|
|
if (!canInstall) {
|
|
$scope.appInstall.busy = false;
|
|
$scope.appInstallForm.location.$setPristine();
|
|
$('#appInstallLocationInput').focus();
|
|
return;
|
|
}
|
|
|
|
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, data, function (error, newAppId) {
|
|
if (error) {
|
|
var errorMessage = error.message.toLowerCase();
|
|
|
|
if (error.statusCode === 402) {
|
|
$scope.appInstall.state = 'subscriptionRequired';
|
|
$scope.appInstall.subscriptionErrorMesssage = error.message;
|
|
$('#collapseMediaLinksCarousel').collapse('hide');
|
|
$('#collapseResourceConstraint').collapse('hide');
|
|
$('#collapseInstallForm').collapse('hide');
|
|
$('#collapseSubscriptionRequired').collapse('show');
|
|
} else if (error.statusCode === 409) {
|
|
if (errorMessage.indexOf('port') !== -1) {
|
|
$scope.appInstall.error.port = error.message;
|
|
} else if (errorMessage.indexOf('location') !== -1) {
|
|
if (errorMessage.indexOf('primary') !== -1) {
|
|
$scope.appInstall.error.location = error.message;
|
|
$scope.appInstallForm.location.$setPristine();
|
|
$('#appInstallLocationInput').focus();
|
|
} else {
|
|
$scope.appInstall.error.secondaryDomain = error.message;
|
|
}
|
|
} else {
|
|
$scope.appInstall.error.other = error.message;
|
|
}
|
|
} else if (error.statusCode === 400) {
|
|
$scope.appInstall.error.other = error.message;
|
|
} 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;
|
|
}
|
|
|
|
$scope.showView('/apps');
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.appNotFound = {
|
|
appId: '',
|
|
version: ''
|
|
};
|
|
|
|
$scope.appstoreLogin = {
|
|
busy: false,
|
|
error: {},
|
|
email: '',
|
|
password: '',
|
|
totpToken: '',
|
|
setupType: 'login',
|
|
termsAccepted: false,
|
|
setupToken: '',
|
|
|
|
submit: function () {
|
|
$scope.appstoreLogin.error = {};
|
|
$scope.appstoreLogin.busy = true;
|
|
|
|
var func = $scope.appstoreLogin.setupToken ? Client.registerCloudronWithSetupToken.bind(null, $scope.appstoreLogin.setupToken) : Client.registerCloudron.bind(null, $scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.setupType === 'register');
|
|
func(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.appstoreSignupForm.email.$setPristine();
|
|
$scope.appstoreSignupForm.password.$setPristine();
|
|
$('#inputAppstoreLoginEmail').focus();
|
|
} else if (error.statusCode === 412) {
|
|
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.loginPassword = 'Wrong email or password';
|
|
$scope.appstoreLogin.password = '';
|
|
$('#inputAppstoreLoginPassword').focus();
|
|
$scope.appstoreLoginForm.password.$setPristine();
|
|
}
|
|
} else 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();
|
|
$scope.appstoreSignupForm.email.$setPristine();
|
|
$scope.appstoreSignupForm.password.$setPristine();
|
|
$('#inputAppstoreLoginEmail').focus();
|
|
} else {
|
|
console.error(error);
|
|
$scope.appstoreLogin.error.generic = error.message;
|
|
}
|
|
} else if (error.statusCode === 402) {
|
|
$scope.appstoreLogin.error.setupToken = 'Invalid or expired setup token';
|
|
$scope.appstoreLogin.setupToken = '';
|
|
$scope.appstoreSetupTokenForm.setupToken.$setPristine();
|
|
$('#inputAppstoreSetupToken').focus();
|
|
} else {
|
|
console.error(error);
|
|
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// do a full re-init of the view now that we have a subscription
|
|
init();
|
|
});
|
|
}
|
|
};
|
|
|
|
// TODO does not support testing apps in search
|
|
$scope.search = function () {
|
|
if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);
|
|
|
|
$scope.category = '';
|
|
|
|
Client.getAppstoreAppsFast(function (error, apps) {
|
|
if (error) return $timeout($scope.search, 1000);
|
|
|
|
var token = $scope.searchString.toUpperCase();
|
|
|
|
$scope.popularApps = [];
|
|
$scope.apps = apps.filter(function (app) {
|
|
// on searches we give highe priority if title or tagline matches
|
|
app.priority = 0;
|
|
|
|
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) {
|
|
app.priority = 2;
|
|
return true;
|
|
}
|
|
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) {
|
|
app.priority = 1;
|
|
return true;
|
|
}
|
|
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
|
|
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
|
|
return false;
|
|
});
|
|
});
|
|
};
|
|
|
|
function filterForNewApps(apps) {
|
|
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
|
var tmp = [];
|
|
var i = 0;
|
|
|
|
do {
|
|
var offset = moment().subtract(i++, 'days');
|
|
tmp = apps.filter(function (app) { return moment(app.publishedAt).isAfter(offset); });
|
|
} while(tmp.length < minApps);
|
|
|
|
return tmp;
|
|
}
|
|
|
|
function filterForRecentlyUpdatedApps(apps) {
|
|
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
|
var tmp = [];
|
|
var i = 0;
|
|
|
|
do {
|
|
var offset = moment().subtract(i++, 'days');
|
|
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); }); // creationDate here is from appstore's appversions table
|
|
} while(tmp.length < minApps);
|
|
|
|
return tmp;
|
|
}
|
|
|
|
$scope.showCategory = function (category) {
|
|
$scope.category = category;
|
|
|
|
$scope.cachedCategory = $scope.category;
|
|
|
|
Client.getAppstoreAppsFast(function (error, apps) {
|
|
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
|
|
|
|
if (!$scope.category) {
|
|
$scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
|
|
$scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
|
} else if ($scope.category === 'new') {
|
|
$scope.apps = filterForNewApps(apps);
|
|
} else if ($scope.category === 'recent') {
|
|
$scope.apps = filterForRecentlyUpdatedApps(apps);
|
|
} else {
|
|
$scope.apps = apps.filter(function (app) {
|
|
return app.manifest.tags.some(function (tag) { return $scope.category.toUpperCase() === tag.toUpperCase(); }); // reverse sort;
|
|
}).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
|
}
|
|
|
|
// ensure we scroll to top
|
|
document.getElementById('ng-view').scrollTop = 0;
|
|
});
|
|
};
|
|
|
|
$scope.openSubscriptionSetup = function () {
|
|
Client.getSubscription(function (error, subscription) {
|
|
if (error) return console.error('Unable to get subscription.', error);
|
|
|
|
Client.openSubscriptionSetup(subscription);
|
|
});
|
|
};
|
|
|
|
$scope.showAppNotFound = function (appId, version) {
|
|
$scope.appNotFound.appId = appId;
|
|
$scope.appNotFound.version = version || 'latest';
|
|
|
|
$('#appNotFoundModal').modal('show');
|
|
};
|
|
|
|
$scope.gotoApp = function (app) {
|
|
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
|
};
|
|
|
|
$scope.openAppProxy = function () {
|
|
$location.path('/appstore/io.cloudron.builtin.appproxy', false).search({});
|
|
};
|
|
|
|
$scope.applinksAdd = {
|
|
error: {},
|
|
busy: false,
|
|
upstreamUri: '',
|
|
label: '',
|
|
tags: '',
|
|
accessRestrictionOption: 'any',
|
|
accessRestriction: { users: [], groups: [] },
|
|
|
|
isAccessRestrictionValid: function () {
|
|
return !!($scope.applinksAdd.accessRestriction.users.length || $scope.applinksAdd.accessRestriction.groups.length);
|
|
},
|
|
|
|
show: function () {
|
|
$scope.applinksAdd.error = {};
|
|
$scope.applinksAdd.busy = false;
|
|
$scope.applinksAdd.upstreamUri = '';
|
|
$scope.applinksAdd.label = '';
|
|
$scope.applinksAdd.tags = '';
|
|
|
|
$scope.applinksAddForm.$setUntouched();
|
|
$scope.applinksAddForm.$setPristine();
|
|
|
|
$('#applinksAddModal').modal('show');
|
|
|
|
return false; // prevent propagation and default
|
|
},
|
|
|
|
submit: function () {
|
|
if (!$scope.applinksAdd.upstreamUri) return;
|
|
|
|
$scope.applinksAdd.busy = true;
|
|
$scope.applinksAdd.error = {};
|
|
|
|
var accessRestriction = null;
|
|
if ($scope.applinksAdd.accessRestrictionOption === 'groups') {
|
|
accessRestriction = { users: [], groups: [] };
|
|
accessRestriction.users = $scope.applinksAdd.accessRestriction.users.map(function (u) { return u.id; });
|
|
accessRestriction.groups = $scope.applinksAdd.accessRestriction.groups.map(function (g) { return g.id; });
|
|
}
|
|
|
|
var data = {
|
|
upstreamUri: $scope.applinksAdd.upstreamUri,
|
|
label: $scope.applinksAdd.label,
|
|
accessRestriction: accessRestriction,
|
|
tags: $scope.applinksAdd.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
|
};
|
|
|
|
Client.addApplink(data, function (error) {
|
|
$scope.applinksAdd.busy = false;
|
|
|
|
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
|
$scope.applinksAdd.error.upstreamUri = error.message;
|
|
$scope.applinksAddForm.$setUntouched();
|
|
$scope.applinksAddForm.$setPristine();
|
|
return;
|
|
}
|
|
if (error) return console.error('Failed to add applink', error);
|
|
|
|
$scope.showView('/apps');
|
|
});
|
|
}
|
|
};
|
|
|
|
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) {
|
|
Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
|
|
if (error) {
|
|
$scope.showAppNotFound(appId, version);
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
$scope.appInstall.show(result);
|
|
});
|
|
} else {
|
|
$scope.appInstall.reset();
|
|
}
|
|
});
|
|
}
|
|
|
|
function fetchUsers() {
|
|
Client.getAllUsers(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(fetchGroups, 5000);
|
|
}
|
|
|
|
$scope.groups = groups;
|
|
});
|
|
}
|
|
|
|
function fetchMemory() {
|
|
Client.memory(function (error, memory) {
|
|
if (error) {
|
|
console.error(error);
|
|
return $timeout(fetchMemory, 5000);
|
|
}
|
|
|
|
$scope.memory = memory;
|
|
});
|
|
}
|
|
|
|
function getSubscription(callback) {
|
|
var validSubscription = false;
|
|
|
|
Client.getSubscription(function (error, subscription) {
|
|
if (error) {
|
|
if (error.statusCode === 412) { // not registered yet
|
|
validSubscription = false;
|
|
} else if (error.statusCode === 402) { // invalid token, license error
|
|
validSubscription = false;
|
|
} else { // 424/external error?
|
|
return callback(error);
|
|
}
|
|
} else {
|
|
validSubscription = true;
|
|
$scope.subscription = subscription;
|
|
}
|
|
|
|
// clear busy state when a login/signup was performed
|
|
$scope.appstoreLogin.busy = false;
|
|
|
|
// also update the root controller status
|
|
if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();
|
|
|
|
callback(null, validSubscription);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
Client.getAppstoreAppsFast(function (error) {
|
|
if (error && error.statusCode === 402) {
|
|
$scope.validSubscription = false;
|
|
$scope.ready = true;
|
|
return;
|
|
} else if (error) {
|
|
console.error('Failed to get apps. Will retry.', error);
|
|
$timeout(init, 1000);
|
|
return;
|
|
}
|
|
|
|
$scope.showCategory('');
|
|
|
|
getSubscription(function (error, validSubscription) {
|
|
if (error) console.error('Failed to get subscription.', error);
|
|
|
|
// autofocus login
|
|
if (!validSubscription) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
|
|
|
$scope.validSubscription = validSubscription;
|
|
$scope.ready = true;
|
|
|
|
// refresh everything in background
|
|
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
|
Client.refreshConfig(); // refresh domain, user, group limit etc
|
|
fetchUsers();
|
|
fetchGroups();
|
|
fetchMemory();
|
|
|
|
// domains is required since we populate the dropdown with domains[0]
|
|
Client.getDomains(function (error, result) {
|
|
if (error) return console.error('Error getting domains.', 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);
|
|
|
|
setTimeout(function () { $('#appstoreSearch').focus(); }, 1);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Client.onReady(init);
|
|
|
|
$('#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();
|
|
});
|
|
});
|
|
|
|
$('.modal-backdrop').remove();
|
|
}]);
|