2018-01-22 13:01:38 -08:00
'use strict' ;
2024-10-11 11:28:08 +02:00
/* global angular */
/* global $ */
2022-02-07 22:22:10 -08:00
/* global async */
2019-09-24 18:50:52 +02:00
/* global ERROR */
2020-06-18 10:25:21 -07:00
/* global RSTATES */
2019-10-22 17:51:09 +02:00
/* global moment */
2019-01-22 10:54:03 +01:00
2020-12-09 12:15:08 +01:00
angular . module ( 'Application' ) . controller ( 'AppStoreController' , [ '$scope' , '$translate' , '$location' , '$timeout' , '$routeParams' , 'Client' , function ( $scope , $translate , $location , $timeout , $routeParams , Client ) {
2020-02-24 12:56:13 +01:00
Client . onReady ( function ( ) { if ( ! Client . getUserInfo ( ) . isAtLeastAdmin ) $location . path ( '/' ) ; } ) ;
2018-01-22 13:01:38 -08:00
2024-08-20 18:16:57 +02:00
$scope . HOST _PORT _MIN = 1 ;
2018-01-22 13:01:38 -08:00
$scope . HOST _PORT _MAX = 65535 ;
$scope . ready = false ;
$scope . apps = [ ] ;
2021-03-18 14:14:22 +01:00
$scope . popularApps = [ ] ;
2018-01-22 13:01:38 -08:00
$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 = '' ;
2019-05-04 21:57:42 -07:00
$scope . validSubscription = false ;
$scope . subscription = { } ;
2019-12-20 10:02:01 -08:00
$scope . memory = null ; // { memory, swap }
2022-11-10 17:44:11 +01:00
2018-01-22 13:01:38 -08:00
$scope . showView = function ( view ) {
2024-10-11 11:28:08 +02:00
$ ( '#appInstallModal' ) . off ( 'hidden.bs.modal' ) ;
2018-01-22 13:01:38 -08:00
// 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 ( ) {
2024-10-11 11:28:08 +02:00
$scope . appInstall . reset ( ) ;
$ ( '.modal' ) . off ( 'hidden.bs.modal' ) ;
$location . path ( view ) . search ( { } ) ;
2018-01-22 13:01:38 -08:00
} ) ;
$ ( '.modal' ) . modal ( 'hide' ) ;
} ;
2020-12-09 12:15:08 +01:00
// If new categories added make sure the translation below exists
2020-06-12 15:02:41 +02:00
$scope . categories = [
{ id : 'analytics' , icon : 'fa fa-chart-line' , label : 'Analytics' } ,
2023-06-04 18:18:22 +02:00
{ id : 'automation' , icon : 'fa fa-robot' , label : 'Automation' } ,
2020-06-12 15:02:41 +02:00
{ 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' } ,
2021-01-11 21:32:15 -08:00
{ id : 'federated' , icon : 'fa fa-project-diagram' , label : 'Federated' } ,
2020-06-12 15:02:41 +02:00
{ id : 'finance' , icon : 'fa fa-hand-holding-usd' , label : 'Finance' } ,
{ id : 'forum' , icon : 'fa fa-users' , label : 'Forum' } ,
2023-03-24 05:52:46 +01:00
{ id : 'fun' , icon : 'fa fa-gamepad' , label : 'Fun' } ,
2020-06-12 15:02:41 +02:00
{ id : 'gallery' , icon : 'fa fa-images' , label : 'Gallery' } ,
{ id : 'game' , icon : 'fa fa-gamepad' , label : 'Games' } ,
2021-03-30 15:03:51 +02:00
{ id : 'git' , icon : 'fa fa-code-branch' , label : 'Code Hosting' } ,
{ id : 'hosting' , icon : 'fa fa-server' , label : 'Web Hosting' } ,
2020-10-27 08:48:48 +01:00
{ id : 'learning' , icon : 'fas fa-graduation-cap' , label : 'Learning' } ,
2020-12-08 10:40:36 -08:00
{ id : 'media' , icon : 'fas fa-photo-video' , label : 'Media' } ,
2023-03-24 05:40:50 +01:00
{ id : 'no-code' , icon : 'fas fa-code' , label : 'No-code' } ,
2020-06-12 15:02:41 +02:00
{ id : 'notes' , icon : 'fa fa-sticky-note' , label : 'Notes' } ,
{ id : 'project' , icon : 'fas fa-project-diagram' , label : 'Project Management' } ,
2021-03-30 15:03:51 +02:00
{ id : 'sync' , icon : 'fa fa-sync-alt' , label : 'File Sync' } ,
2023-11-03 10:24:38 +01:00
{ id : 'voip' , icon : 'fa fa-headset' , label : 'VoIP' } ,
2020-06-12 15:02:41 +02:00
{ id : 'vpn' , icon : 'fa fa-user-secret' , label : 'VPN' } ,
{ id : 'wiki' , icon : 'fab fa-wikipedia-w' , label : 'Wiki' } ,
] ;
2020-12-09 12:15:08 +01:00
// 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 ] ;
} ) ;
} ) ;
2020-06-13 22:56:24 +02:00
$scope . categoryButtonLabel = function ( category ) {
2021-03-30 15:03:51 +02:00
var categoryLabel = $translate . instant ( 'appstore.categoryLabel' ) ;
2020-12-15 15:58:13 +01:00
2022-11-09 15:45:34 +01:00
if ( category === '' ) return $translate . instant ( 'appstore.category.all' ) ;
if ( category === 'new' ) return $translate . instant ( 'appstore.category.newApps' ) ;
2020-06-13 22:56:24 +02:00
var tmp = $scope . categories . find ( function ( c ) { return c . id === category ; } ) ;
if ( tmp ) return tmp . label ;
2020-12-15 15:58:13 +01:00
return categoryLabel ;
2020-06-13 22:56:24 +02:00
} ;
2022-06-09 14:21:50 +02:00
$scope . isProxyApp = function ( app ) {
if ( ! app ) return false ;
return app . id === 'io.cloudron.builtin.appproxy' ;
} ;
2022-05-25 21:10:53 +02:00
$scope . userManagementFilterOptions = [
2022-05-25 21:13:39 +02:00
{ id : '' , icon : '' , label : $translate . instant ( 'appstore.ssofilter.all' ) } ,
2022-05-25 21:10:53 +02:00
{ 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 ;
} ;
2018-01-22 13:01:38 -08:00
$scope . appInstall = {
busy : false ,
state : 'appInfo' ,
error : { } ,
app : { } ,
2019-09-23 23:59:48 +02:00
needsOverwrite : false ,
2022-01-16 18:29:32 -08:00
subdomain : '' ,
2022-01-20 16:58:00 -08:00
domain : null , // object and not the string
secondaryDomains : { } ,
2024-07-16 22:21:36 +02:00
ports : { } ,
portsEnabled : { } ,
2018-01-22 13:01:38 -08:00
mediaLinks : [ ] ,
keyFile : null ,
keyFileName : '' ,
2021-02-17 17:09:46 +01:00
accessRestrictionOption : '' ,
2018-01-22 13:01:38 -08:00
accessRestriction : { users : [ ] , groups : [ ] } ,
customAuth : false ,
optionalSso : false ,
2018-09-05 17:13:59 +02:00
subscriptionErrorMesssage : '' ,
2022-06-09 14:21:50 +02:00
upstreamUri : '' ,
2018-01-22 13:01:38 -08:00
isAccessRestrictionValid : function ( ) {
var tmp = $scope . appInstall . accessRestriction ;
return ! ! ( tmp . users . length || tmp . groups . length ) ;
} ,
reset : function ( ) {
$scope . appInstall . app = { } ;
$scope . appInstall . error = { } ;
2019-09-23 23:59:48 +02:00
$scope . appInstall . needsOverwrite = false ;
2022-01-16 18:29:32 -08:00
$scope . appInstall . subdomain = '' ;
2018-01-22 13:01:38 -08:00
$scope . appInstall . domain = null ;
2022-01-20 16:58:00 -08:00
$scope . appInstall . secondaryDomains = { } ;
2024-07-16 22:21:36 +02:00
$scope . appInstall . ports = { } ;
2018-01-22 13:01:38 -08:00
$scope . appInstall . state = 'appInfo' ;
$scope . appInstall . mediaLinks = [ ] ;
$scope . appInstall . keyFile = null ;
$scope . appInstall . keyFileName = '' ;
2021-02-17 17:09:46 +01:00
$scope . appInstall . accessRestrictionOption = '' ;
2018-01-22 13:01:38 -08:00
$scope . appInstall . accessRestriction = { users : [ ] , groups : [ ] } ;
$scope . appInstall . optionalSso = false ;
$scope . appInstall . customAuth = false ;
2018-09-05 17:13:59 +02:00
$scope . appInstall . subscriptionErrorMesssage = '' ;
2022-06-09 14:21:50 +02:00
$scope . appInstall . upstreamUri = '' ;
2018-01-22 13:01:38 -08:00
$ ( '#collapseInstallForm' ) . collapse ( 'hide' ) ;
$ ( '#collapseResourceConstraint' ) . collapse ( 'hide' ) ;
2019-08-14 16:38:02 +02:00
$ ( '#collapseSubscriptionRequired' ) . collapse ( 'hide' ) ;
2018-05-28 20:26:18 +02:00
$ ( '#collapseMediaLinksCarousel' ) . collapse ( 'show' ) ;
2018-01-22 13:01:38 -08:00
if ( $scope . appInstallForm ) {
$scope . appInstallForm . $setPristine ( ) ;
$scope . appInstallForm . $setUntouched ( ) ;
}
} ,
showForm : function ( force ) {
2019-12-20 10:02:01 -08:00
var app = $scope . appInstall . app ;
var DEFAULT _MEMORY _LIMIT = 1024 * 1024 * 256 ;
2024-04-10 12:12:24 +02:00
var needed = app . manifest . memoryLimit || DEFAULT _MEMORY _LIMIT ; // RAM
2020-06-18 10:25:21 -07:00
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 ) ;
2024-04-10 12:12:24 +02:00
var totalMemory = $scope . memory . memory * 2 ;
2019-12-20 10:02:01 -08:00
var available = ( totalMemory || 0 ) - used ;
var enoughResourcesAvailable = ( available - needed ) >= 0 ;
if ( enoughResourcesAvailable || force ) {
2018-01-22 13:01:38 -08:00
$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' ) ;
}
} ,
2018-08-03 22:04:06 -07:00
show : function ( app ) { // this is an appstore app object!
2018-01-22 13:01:38 -08:00
$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 || [ ] ;
2018-03-13 09:38:55 +01:00
$scope . appInstall . domain = $scope . domains . find ( function ( d ) { return $scope . config . adminDomain === d . domain ; } ) ; // pre-select the adminDomain
2022-01-20 16:58:00 -08:00
$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
} ;
}
2024-07-16 22:21:36 +02:00
$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
2018-01-22 13:01:38 -08:00
var manifest = app . manifest ;
$scope . appInstall . optionalSso = ! ! manifest . optionalSso ;
2023-04-25 19:52:14 +02:00
$scope . appInstall . customAuth = ! ( manifest . addons [ 'ldap' ] || manifest . addons [ 'oidc' ] || manifest . addons [ 'proxyAuth' ] ) ;
2018-08-03 22:19:07 -07:00
2021-03-02 09:47:24 +01:00
$scope . appInstall . accessRestrictionOption = $scope . groups . length ? '' : 'any' ; // make the user select an ACL conciously if groups are used
2020-02-25 13:00:36 +01:00
$scope . appInstall . accessRestriction = { users : [ ] , groups : [ ] } ;
2018-01-22 13:01:38 -08:00
// set default ports
2018-08-13 08:38:47 -07:00
var allPorts = angular . extend ( { } , $scope . appInstall . app . manifest . tcpPorts , $scope . appInstall . app . manifest . udpPorts ) ;
for ( var env in allPorts ) {
2024-07-16 22:21:36 +02:00
$scope . appInstall . ports [ env ] = allPorts [ env ] . defaultValue || 0 ;
$scope . appInstall . portsEnabled [ env ] = true ;
2018-01-22 13:01:38 -08:00
}
$ ( '#appInstallModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . appInstall . busy = true ;
$scope . appInstall . error . other = null ;
$scope . appInstall . error . location = null ;
$scope . appInstall . error . port = null ;
2022-01-20 16:58:00 -08:00
var secondaryDomains = { } ;
for ( var env2 in $scope . appInstall . secondaryDomains ) {
secondaryDomains [ env2 ] = {
subdomain : $scope . appInstall . secondaryDomains [ env2 ] . subdomain ,
domain : $scope . appInstall . secondaryDomains [ env2 ] . domain . domain
} ;
}
2024-07-16 22:21:36 +02:00
// 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 ] ;
2018-01-22 13:01:38 -08:00
}
}
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 = {
2019-09-24 00:21:01 +02:00
overwriteDns : $scope . appInstall . needsOverwrite ,
2022-01-16 18:29:32 -08:00
subdomain : $scope . appInstall . subdomain || '' ,
2018-01-22 13:01:38 -08:00
domain : $scope . appInstall . domain . domain ,
2022-01-20 16:58:00 -08:00
secondaryDomains : secondaryDomains ,
2024-07-16 22:21:36 +02:00
ports : finalPorts ,
2018-01-22 13:01:38 -08:00
accessRestriction : finalAccessRestriction ,
2022-06-09 14:21:50 +02:00
sso : ! $scope . appInstall . optionalSso ? undefined : ( $scope . appInstall . accessRestrictionOption !== 'nosso' ) ,
2018-01-22 13:01:38 -08:00
} ;
2022-09-29 18:45:38 +02:00
if ( $scope . appInstall . upstreamUri ) {
data . upstreamUri = $scope . appInstall . upstreamUri ;
data . upstreamUri = data . upstreamUri . replace ( /\/$/ , '' ) ;
}
2022-06-11 12:51:26 -07:00
2022-02-07 22:22:10 -08:00
var domains = [ ] ;
domains . push ( { subdomain : data . subdomain , domain : data . domain , type : 'primary' } ) ;
var canInstall = true ;
2019-09-23 23:47:33 +02:00
2022-02-07 22:22:10 -08:00
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 ;
2019-09-24 00:59:12 -07:00
} else {
2022-02-07 22:22:10 -08:00
$scope . appInstall . error . secondaryDomain = message ;
}
} else {
if ( domain . type === 'primary' ) {
2019-09-24 00:59:12 -07:00
$scope . appInstall . error . location = result . error . message ;
2022-02-07 22:22:10 -08:00
} else {
$scope . appInstall . error . secondaryDomain = message ;
2019-09-24 00:59:12 -07:00
}
2022-02-07 22:22:10 -08:00
}
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 ;
2019-09-24 00:59:12 -07:00
} else {
2022-02-07 22:22:10 -08:00
$scope . appInstall . error . secondaryDomain = message ;
2019-09-24 00:59:12 -07:00
}
2022-02-07 22:22:10 -08:00
$scope . appInstall . needsOverwrite = true ;
canInstall = false ;
2019-09-23 23:47:33 +02:00
}
2022-02-07 22:22:10 -08:00
callback ( ) ;
} ) ;
} , function ( error ) {
if ( error ) {
2022-10-13 21:10:55 +02:00
$scope . appInstall . busy = false ;
2022-02-07 22:22:10 -08:00
return Client . error ( error ) ;
}
if ( ! canInstall ) {
$scope . appInstall . busy = false ;
$scope . appInstallForm . location . $setPristine ( ) ;
$ ( '#appInstallLocationInput' ) . focus ( ) ;
return ;
2019-09-23 23:47:33 +02:00
}
2024-12-10 11:53:29 +01:00
Client . installApp ( $scope . appInstall . app . id , $scope . appInstall . app . manifest , data , function ( error , newAppId ) {
2019-09-23 23:47:33 +02:00
if ( error ) {
2022-02-07 16:11:57 -08:00
var errorMessage = error . message . toLowerCase ( ) ;
2019-09-23 23:47:33 +02:00
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 ) {
2022-02-07 16:11:57 -08:00
if ( errorMessage . indexOf ( 'port' ) !== - 1 ) {
2019-09-23 23:47:33 +02:00
$scope . appInstall . error . port = error . message ;
2022-02-07 16:11:57 -08:00
} else if ( errorMessage . indexOf ( 'location' ) !== - 1 ) {
if ( errorMessage . indexOf ( 'primary' ) !== - 1 ) {
2022-01-20 16:58:00 -08:00
$scope . appInstall . error . location = error . message ;
$scope . appInstallForm . location . $setPristine ( ) ;
$ ( '#appInstallLocationInput' ) . focus ( ) ;
} else {
$scope . appInstall . error . secondaryDomain = error . message ;
}
2019-09-23 23:47:33 +02:00
} else {
$scope . appInstall . error . other = error . message ;
}
} else if ( error . statusCode === 400 ) {
2024-12-10 14:48:07 +01:00
$scope . appInstall . error . other = error . message ;
2019-09-01 21:38:30 -07:00
} else {
$scope . appInstall . error . other = error . message ;
}
2019-09-23 23:47:33 +02:00
$scope . appInstall . busy = false ;
return ;
2018-01-22 13:01:38 -08:00
}
$scope . appInstall . busy = false ;
2019-09-23 23:47:33 +02:00
// stash new app id for later
$scope . appInstall . app . id = newAppId ;
2018-06-14 15:46:55 +02:00
2019-09-23 23:47:33 +02:00
// 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 ;
}
2018-01-22 13:01:38 -08:00
2024-10-11 11:28:08 +02:00
$scope . showView ( '/apps' ) ;
2019-09-23 23:47:33 +02:00
} ) ;
2018-06-14 16:07:27 +02:00
} ) ;
2018-01-22 13:01:38 -08:00
}
} ;
$scope . appNotFound = {
appId : '' ,
version : ''
} ;
$scope . appstoreLogin = {
busy : false ,
error : { } ,
email : '' ,
password : '' ,
2018-04-22 18:52:37 +02:00
totpToken : '' ,
2023-12-02 11:23:03 +01:00
setupType : 'login' ,
2018-01-22 13:01:38 -08:00
termsAccepted : false ,
2023-12-02 11:23:03 +01:00
setupToken : '' ,
2018-01-22 13:01:38 -08:00
submit : function ( ) {
$scope . appstoreLogin . error = { } ;
$scope . appstoreLogin . busy = true ;
2023-12-02 18:20:11 +01:00
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' ) ;
2023-12-02 11:23:03 +01:00
func ( function ( error ) {
2018-01-22 13:01:38 -08:00
if ( error ) {
$scope . appstoreLogin . busy = false ;
if ( error . statusCode === 409 ) {
$scope . appstoreLogin . error . email = 'An account with this email already exists' ;
$scope . appstoreLogin . password = '' ;
2023-12-02 16:42:03 +01:00
$scope . appstoreSignupForm . email . $setPristine ( ) ;
$scope . appstoreSignupForm . password . $setPristine ( ) ;
2018-01-22 13:01:38 -08:00
$ ( '#inputAppstoreLoginEmail' ) . focus ( ) ;
2019-05-04 21:57:42 -07:00
} 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 {
2023-12-02 18:20:11 +01:00
$scope . appstoreLogin . error . loginPassword = 'Wrong email or password' ;
2019-05-04 21:57:42 -07:00
$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 ( ) ;
2023-12-02 16:42:03 +01:00
$scope . appstoreSignupForm . email . $setPristine ( ) ;
$scope . appstoreSignupForm . password . $setPristine ( ) ;
2019-05-04 21:57:42 -07:00
$ ( '#inputAppstoreLoginEmail' ) . focus ( ) ;
} else {
console . error ( error ) ;
$scope . appstoreLogin . error . generic = error . message ;
}
2023-12-02 16:42:03 +01:00
} else if ( error . statusCode === 402 ) {
$scope . appstoreLogin . error . setupToken = 'Invalid or expired setup token' ;
$scope . appstoreLogin . setupToken = '' ;
$scope . appstoreSetupTokenForm . setupToken . $setPristine ( ) ;
$ ( '#inputAppstoreSetupToken' ) . focus ( ) ;
2018-01-22 13:01:38 -08:00
} else {
console . error ( error ) ;
2019-05-06 20:05:12 -07:00
$scope . appstoreLogin . error . generic = error . message || 'Please retry later' ;
2018-01-22 13:01:38 -08:00
}
return ;
}
2019-05-05 07:46:06 -07:00
2022-01-27 17:14:51 +01:00
// do a full re-init of the view now that we have a subscription
init ( ) ;
2018-01-22 13:01:38 -08:00
} ) ;
}
} ;
// TODO does not support testing apps in search
$scope . search = function ( ) {
2020-06-18 17:13:20 +02:00
if ( ! $scope . searchString ) return $scope . showCategory ( $scope . cachedCategory ) ;
2018-01-22 13:01:38 -08:00
$scope . category = '' ;
2023-04-02 18:03:41 +02:00
Client . getAppstoreAppsFast ( function ( error , apps ) {
2018-01-22 13:01:38 -08:00
if ( error ) return $timeout ( $scope . search , 1000 ) ;
var token = $scope . searchString . toUpperCase ( ) ;
2021-03-18 14:14:22 +01:00
$scope . popularApps = [ ] ;
2018-01-22 13:01:38 -08:00
$scope . apps = apps . filter ( function ( app ) {
2022-06-27 18:03:32 +02:00
// 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 ;
}
2018-01-22 13:01:38 -08:00
if ( app . manifest . id . toUpperCase ( ) . indexOf ( token ) !== - 1 ) return true ;
if ( app . manifest . description . toUpperCase ( ) . indexOf ( token ) !== - 1 ) return true ;
2022-06-27 18:03:32 +02:00
if ( app . manifest . tags . join ( ) . toUpperCase ( ) . indexOf ( token ) !== - 1 ) return true ;
2018-01-22 13:01:38 -08:00
return false ;
2022-01-16 18:29:32 -08:00
} ) ;
2018-01-22 13:01:38 -08:00
} ) ;
} ;
2019-11-18 22:43:33 +01:00
function filterForNewApps ( apps ) {
2020-12-15 13:48:53 -08:00
var minApps = apps . length < 12 ? apps . length : 12 ; // prevent endless loop
2019-11-18 22:43:33 +01:00
var tmp = [ ] ;
var i = 0 ;
do {
var offset = moment ( ) . subtract ( i ++ , 'days' ) ;
2022-11-10 19:54:07 +01:00
tmp = apps . filter ( function ( app ) { return moment ( app . publishedAt ) . isAfter ( offset ) ; } ) ;
2019-11-18 22:43:33 +01:00
} while ( tmp . length < minApps ) ;
return tmp ;
}
2019-10-22 17:51:09 +02:00
function filterForRecentlyUpdatedApps ( apps ) {
2020-12-15 13:48:53 -08:00
var minApps = apps . length < 12 ? apps . length : 12 ; // prevent endless loop
2019-10-22 17:51:09 +02:00
var tmp = [ ] ;
var i = 0 ;
do {
var offset = moment ( ) . subtract ( i ++ , 'days' ) ;
2020-12-06 11:32:05 -08:00
tmp = apps . filter ( function ( app ) { return moment ( app . creationDate ) . isAfter ( offset ) ; } ) ; // creationDate here is from appstore's appversions table
2019-10-22 17:51:09 +02:00
} while ( tmp . length < minApps ) ;
return tmp ;
}
2020-06-12 15:02:41 +02:00
$scope . showCategory = function ( category ) {
$scope . category = category ;
2018-01-22 13:01:38 -08:00
$scope . cachedCategory = $scope . category ;
2023-04-02 18:03:41 +02:00
Client . getAppstoreAppsFast ( function ( error , apps ) {
2020-06-12 15:02:41 +02:00
if ( error ) return $timeout ( $scope . showCategory . bind ( null , category ) , 1000 ) ;
2018-01-22 13:01:38 -08:00
if ( ! $scope . category ) {
2021-03-18 14:14:22 +01:00
$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 ; } ) ;
2019-11-18 22:43:33 +01:00
} else if ( $scope . category === 'new' ) {
$scope . apps = filterForNewApps ( apps ) ;
2019-10-22 17:51:09 +02:00
} else if ( $scope . category === 'recent' ) {
$scope . apps = filterForRecentlyUpdatedApps ( apps ) ;
2018-01-22 13:01:38 -08:00
} else {
$scope . apps = apps . filter ( function ( app ) {
2022-02-23 16:29:15 +01:00
return app . manifest . tags . some ( function ( tag ) { return $scope . category . toUpperCase ( ) === tag . toUpperCase ( ) ; } ) ; // reverse sort;
2020-07-17 14:19:25 -07:00
} ) . sort ( function ( a1 , a2 ) { return a2 . ranking - a1 . ranking ; } ) ;
2018-01-22 13:01:38 -08:00
}
2020-06-16 11:37:10 +02:00
// ensure we scroll to top
document . getElementById ( 'ng-view' ) . scrollTop = 0 ;
2018-01-22 13:01:38 -08:00
} ) ;
} ;
2020-02-21 14:07:46 +01:00
$scope . openSubscriptionSetup = function ( ) {
Client . getSubscription ( function ( error , subscription ) {
if ( error ) return console . error ( 'Unable to get subscription.' , error ) ;
Client . openSubscriptionSetup ( subscription ) ;
} ) ;
} ;
2018-01-22 13:01:38 -08:00
$scope . showAppNotFound = function ( appId , version ) {
$scope . appNotFound . appId = appId ;
2020-11-13 21:58:09 +01:00
$scope . appNotFound . version = version || 'latest' ;
2018-01-22 13:01:38 -08:00
$ ( '#appNotFoundModal' ) . modal ( 'show' ) ;
} ;
$scope . gotoApp = function ( app ) {
$location . path ( '/appstore/' + app . manifest . id , false ) . search ( { version : app . manifest . version } ) ;
} ;
2022-10-05 17:17:22 +02:00
$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 ;
2022-10-06 19:50:47 +02:00
$scope . applinksAdd . error = { } ;
2022-10-05 17:17:22 +02:00
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 ;
2022-10-06 19:50:47 +02:00
if ( error && error . statusCode === 400 && error . message . includes ( 'upstreamUri' ) ) {
$scope . applinksAdd . error . upstreamUri = error . message ;
$scope . applinksAddForm . $setUntouched ( ) ;
$scope . applinksAddForm . $setPristine ( ) ;
return ;
}
2022-10-05 17:17:22 +02:00
if ( error ) return console . error ( 'Failed to add applink' , error ) ;
2024-10-11 11:28:08 +02:00
$scope . showView ( '/apps' ) ;
2022-10-05 17:17:22 +02:00
} ) ;
}
} ;
2018-01-22 13:01:38 -08:00
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 ) {
2019-05-04 18:15:33 -07:00
Client . getAppstoreAppByIdAndVersion ( appId , version || 'latest' , function ( error , result ) {
2019-04-29 14:58:42 +02:00
if ( error ) {
$scope . showAppNotFound ( appId , version ) ;
console . error ( error ) ;
return ;
}
2018-01-22 13:01:38 -08:00
2019-04-29 14:58:42 +02:00
$scope . appInstall . show ( result ) ;
} ) ;
2018-01-22 13:01:38 -08:00
} else {
$scope . appInstall . reset ( ) ;
}
} ) ;
}
function fetchUsers ( ) {
2022-02-14 14:55:04 +01:00
Client . getAllUsers ( function ( error , users ) {
2018-01-22 13:01:38 -08:00
if ( error ) {
console . error ( error ) ;
return $timeout ( fetchUsers , 5000 ) ;
}
$scope . users = users ;
} ) ;
}
function fetchGroups ( ) {
Client . getGroups ( function ( error , groups ) {
if ( error ) {
console . error ( error ) ;
2019-05-05 07:40:11 -07:00
return $timeout ( fetchGroups , 5000 ) ;
2018-01-22 13:01:38 -08:00
}
$scope . groups = groups ;
} ) ;
}
2021-09-23 01:14:55 +02:00
function fetchMemory ( ) {
Client . memory ( function ( error , memory ) {
if ( error ) {
console . error ( error ) ;
return $timeout ( fetchMemory , 5000 ) ;
}
$scope . memory = memory ;
} ) ;
}
2019-05-04 21:57:42 -07:00
function getSubscription ( callback ) {
2022-06-01 17:33:49 +02:00
var validSubscription = false ;
2019-05-04 21:57:42 -07:00
Client . getSubscription ( function ( error , subscription ) {
2019-05-05 07:46:06 -07:00
if ( error ) {
2019-05-06 11:07:19 +02:00
if ( error . statusCode === 412 ) { // not registered yet
2022-06-01 17:33:49 +02:00
validSubscription = false ;
2019-10-24 18:09:48 -07:00
} else if ( error . statusCode === 402 ) { // invalid token, license error
2022-06-01 17:33:49 +02:00
validSubscription = false ;
2019-07-08 09:45:14 -07:00
} else { // 424/external error?
2019-05-05 07:46:06 -07:00
return callback ( error ) ;
}
2019-05-04 21:57:42 -07:00
} else {
2022-06-01 17:33:49 +02:00
validSubscription = true ;
2019-05-04 21:57:42 -07:00
$scope . subscription = subscription ;
}
2018-05-28 20:26:18 +02:00
2019-05-04 21:57:42 -07:00
// clear busy state when a login/signup was performed
$scope . appstoreLogin . busy = false ;
2018-05-28 20:26:18 +02:00
2019-05-04 21:57:42 -07:00
// also update the root controller status
if ( $scope . $parent ) $scope . $parent . updateSubscriptionStatus ( ) ;
2018-04-05 23:27:39 +02:00
2022-06-01 17:33:49 +02:00
callback ( null , validSubscription ) ;
2018-01-22 13:01:38 -08:00
} ) ;
}
function init ( ) {
2023-04-02 18:03:41 +02:00
Client . getAppstoreAppsFast ( function ( error ) {
2021-09-23 01:14:55 +02:00
if ( error && error . statusCode === 402 ) {
$scope . validSubscription = false ;
2022-06-01 17:33:49 +02:00
$scope . ready = true ;
2021-09-23 01:14:55 +02:00
return ;
} else if ( error ) {
2022-09-22 21:03:22 +02:00
console . error ( 'Failed to get apps. Will retry.' , error ) ;
2022-06-01 17:33:49 +02:00
$timeout ( init , 1000 ) ;
return ;
2018-01-22 13:01:38 -08:00
}
2021-09-23 01:14:55 +02:00
$scope . showCategory ( '' ) ;
2018-01-22 13:01:38 -08:00
2022-06-01 17:33:49 +02:00
getSubscription ( function ( error , validSubscription ) {
if ( error ) console . error ( 'Failed to get subscription.' , error ) ;
2018-01-22 13:01:38 -08:00
2023-12-02 18:20:11 +01:00
// autofocus login
if ( ! validSubscription ) setTimeout ( function ( ) { $ ( '[name=appstoreLoginForm]' ) . find ( '[autofocus]:first' ) . focus ( ) ; } , 1000 ) ;
2022-06-01 17:33:49 +02:00
$scope . validSubscription = validSubscription ;
$scope . ready = true ;
2021-09-23 01:14:55 +02:00
2022-06-01 17:33:49 +02:00
// refresh everything in background
2023-04-02 18:03:41 +02:00
Client . getAppstoreApps ( function ( error ) { if ( error ) console . error ( 'Failed to fetch apps.' , error ) ; } ) ;
2022-06-01 17:33:49 +02:00
Client . refreshConfig ( ) ; // refresh domain, user, group limit etc
fetchUsers ( ) ;
fetchGroups ( ) ;
fetchMemory ( ) ;
2021-09-23 01:14:55 +02:00
2022-06-01 17:33:49 +02:00
// 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 ) ;
} ) ;
2019-05-05 11:46:33 -07:00
} ) ;
2018-01-22 13:01:38 -08:00
} ) ;
}
2021-09-23 00:33:40 +02:00
Client . onReady ( init ) ;
2018-01-22 13:01:38 -08:00
$ ( '#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
2018-03-12 19:08:05 +01:00
[ 'appInstallModal' ] . forEach ( function ( id ) {
2018-01-22 13:01:38 -08:00
$ ( '#' + id ) . on ( 'shown.bs.modal' , function ( ) {
2019-05-05 09:05:06 -07:00
$ ( this ) . find ( '[autofocus]:first' ) . focus ( ) ;
2018-01-22 13:01:38 -08:00
} ) ;
} ) ;
$ ( '.modal-backdrop' ) . remove ( ) ;
} ] ) ;