2017-04-13 00:42:44 -07:00
'use strict' ;
exports = module . exports = {
2020-12-29 17:22:32 -08:00
getFeatures ,
2020-02-14 12:20:15 +01:00
2023-08-04 15:34:38 +05:30
getApiServerOrigin ,
getWebServerOrigin ,
getConsoleServerOrigin ,
downloadManifest ,
2020-12-29 17:22:32 -08:00
getApps ,
getApp ,
getAppVersion ,
2023-08-04 15:34:38 +05:30
downloadIcon ,
2019-05-04 11:45:03 -07:00
2023-12-02 11:23:03 +01:00
registerCloudronWithSetupToken ,
registerCloudronWithLogin ,
2021-04-13 14:19:45 -07:00
updateCloudron ,
2019-05-05 13:00:45 -07:00
2020-12-29 17:22:32 -08:00
purchaseApp ,
unpurchaseApp ,
2017-04-13 00:52:02 -07:00
2020-12-29 17:22:32 -08:00
getSubscription ,
isFreePlan ,
2017-06-21 22:17:32 -07:00
2020-12-29 17:22:32 -08:00
getAppUpdate ,
getBoxUpdate ,
2017-04-13 01:23:11 -07:00
2022-04-04 13:54:57 -07:00
createTicket ,
// exported for tests
2023-08-04 15:34:38 +05:30
_setApiServerOrigin : setApiServerOrigin ,
2022-04-04 13:54:57 -07:00
_unregister : unregister
2017-04-13 00:42:44 -07:00
} ;
2021-08-18 15:54:53 -07:00
const apps = require ( './apps.js' ) ,
2018-03-05 17:03:54 +01:00
assert = require ( 'assert' ) ,
2019-10-24 17:28:59 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.js' ) ,
2017-04-13 00:52:02 -07:00
debug = require ( 'debug' ) ( 'box:appstore' ) ,
2017-07-19 23:05:14 +02:00
eventlog = require ( './eventlog.js' ) ,
2024-06-15 17:08:42 +02:00
manifestFormat = require ( 'cloudron-manifestformat' ) ,
2023-08-03 13:38:42 +05:30
network = require ( './network.js' ) ,
2020-10-06 17:53:04 +02:00
path = require ( 'path' ) ,
2020-02-14 12:20:15 +01:00
paths = require ( './paths.js' ) ,
2023-08-04 15:34:38 +05:30
promiseRetry = require ( './promise-retry.js' ) ,
2018-01-25 09:35:06 -08:00
safe = require ( 'safetydance' ) ,
semver = require ( 'semver' ) ,
2017-04-13 00:42:44 -07:00
settings = require ( './settings.js' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'appstore' ) ,
2017-04-13 00:42:44 -07:00
superagent = require ( 'superagent' ) ,
2023-06-26 18:06:37 +05:30
support = require ( './support.js' ) ;
2017-04-13 00:42:44 -07:00
2020-02-28 15:18:16 +01:00
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
2020-02-14 12:20:15 +01:00
let gFeatures = {
2020-06-18 14:34:22 +02:00
userMaxCount : 5 ,
2021-01-13 14:48:58 +01:00
userGroups : false ,
2021-01-13 16:29:25 +01:00
userRoles : false ,
2020-06-18 14:34:22 +02:00
domainMaxCount : 1 ,
externalLdap : false ,
privateDockerRegistry : false ,
branding : false ,
2020-07-09 21:50:58 -07:00
support : false ,
2022-01-13 14:34:02 -08:00
profileConfig : false ,
2020-07-16 18:14:25 +02:00
mailboxMaxCount : 5 ,
emailPremium : false
2020-02-14 12:20:15 +01:00
} ;
// attempt to load feature cache in case appstore would be down
2024-06-15 17:03:54 +02:00
const tmp = safe . JSON . parse ( safe . fs . readFileSync ( paths . FEATURES _INFO _FILE , 'utf8' ) ) ;
2020-02-20 15:45:34 +01:00
if ( tmp ) gFeatures = tmp ;
2020-02-14 12:20:15 +01:00
function getFeatures ( ) {
return gFeatures ;
}
2023-08-04 15:34:38 +05:30
async function getApiServerOrigin ( ) {
return await settings . get ( settings . API _SERVER _ORIGIN _KEY ) || 'https://api.cloudron.io' ;
}
async function setApiServerOrigin ( origin ) {
assert . strictEqual ( typeof origin , 'string' ) ;
await settings . set ( settings . API _SERVER _ORIGIN _KEY , origin ) ;
}
async function getWebServerOrigin ( ) {
return await settings . get ( settings . WEB _SERVER _ORIGIN _KEY ) || 'https://cloudron.io' ;
}
async function getConsoleServerOrigin ( ) {
return await settings . get ( settings . CONSOLE _SERVER _ORIGIN _KEY ) || 'https://console.cloudron.io' ;
}
2021-08-18 15:54:53 -07:00
async function login ( email , password , totpToken ) {
2019-05-03 16:27:47 -07:00
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof totpToken , 'string' ) ;
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/login ` )
2022-03-31 22:41:48 -07:00
. send ( { email , password , totpToken } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Incorrect username or password' ) ;
2022-05-26 22:27:58 -07:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Login error. status code: ${ response . status } ` ) ;
if ( ! response . body . accessToken ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Login error. invalid response: ${ response . text } ` ) ;
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07:00
return response . body ; // { userId, accessToken }
2019-05-03 16:27:47 -07:00
}
2021-08-18 15:54:53 -07:00
async function registerUser ( email , password ) {
2019-05-03 16:27:47 -07:00
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/register_user ` )
2022-05-18 23:30:30 -07:00
. send ( { email , password , utmSource : 'cloudron-dashboard' } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2019-05-03 16:27:47 -07:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2022-05-26 22:27:58 -07:00
if ( response . status === 409 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'Registration error: account already exists' ) ;
if ( response . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Registration error. invalid response: ${ response . status } ` ) ;
2021-08-18 15:54:53 -07:00
}
2020-02-20 17:05:07 +01:00
2021-08-18 15:54:53 -07:00
async function getSubscription ( ) {
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2017-06-21 22:17:32 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/subscription ` )
2021-08-18 15:54:53 -07:00
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2017-06-21 22:17:32 -07:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status === 502 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Stripe error: ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unknown error: ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2020-02-14 12:20:15 +01:00
2021-08-18 15:54:53 -07:00
// update the features cache
gFeatures = response . body . features ;
safe . fs . writeFileSync ( paths . FEATURES _INFO _FILE , JSON . stringify ( gFeatures ) , 'utf8' ) ;
return response . body ;
2017-06-21 22:17:32 -07:00
}
2018-05-29 13:16:36 +02:00
function isFreePlan ( subscription ) {
return ! subscription || subscription . plan . id === 'free' ;
}
2018-08-31 10:53:35 +02:00
// See app.js install it will create a db record first but remove it again if appstore purchase fails
2021-08-18 15:54:53 -07:00
async function purchaseApp ( data ) {
2019-05-03 14:00:21 -07:00
assert . strictEqual ( typeof data , 'object' ) ; // { appstoreId, manifestId, appId }
2018-10-18 14:15:18 -07:00
assert ( data . appstoreId || data . manifestId ) ;
2019-05-03 14:00:21 -07:00
assert . strictEqual ( typeof data . appId , 'string' ) ;
2017-04-13 01:07:07 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/cloudronapps ` )
2021-08-18 15:54:53 -07:00
. send ( data )
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 16:21:21 +01:00
if ( response . status === 404 ) throw new BoxError ( BoxError . NOT _FOUND , 'appstoreId does not exist' ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2021-08-18 15:54:53 -07:00
if ( response . status === 402 ) throw new BoxError ( BoxError . LICENSE _ERROR , response . body . message ) ;
// 200 if already purchased, 201 is newly purchased
2023-06-26 18:06:37 +05:30
if ( response . status !== 201 && response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App purchase failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2017-04-13 01:07:07 -07:00
}
2021-08-18 15:54:53 -07:00
async function unpurchaseApp ( appId , data ) {
2017-04-13 00:42:44 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2019-04-30 23:35:49 -07:00
assert . strictEqual ( typeof data , 'object' ) ; // { appstoreId, manifestId }
2018-10-18 14:15:18 -07:00
assert ( data . appstoreId || data . manifestId ) ;
2017-04-13 00:42:44 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2023-08-04 15:34:38 +05:30
const url = ` ${ await getApiServerOrigin ( ) } /api/v1/cloudronapps/ ${ appId } ` ;
2021-08-18 15:54:53 -07:00
let [ error , response ] = await safe ( superagent . get ( url )
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2021-08-18 15:54:53 -07:00
if ( response . status === 404 ) return ; // was never purchased
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2022-05-26 22:27:58 -07:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App unpurchase failed to get app. status: ${ response . status } ` ) ;
2021-08-18 15:54:53 -07:00
[ error , response ] = await safe ( superagent . del ( url )
. send ( data )
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2022-05-26 22:27:58 -07:00
if ( response . status !== 204 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App unpurchase failed. status: ${ response . status } ` ) ;
2017-04-13 00:42:44 -07:00
}
2017-04-13 00:52:02 -07:00
2021-08-18 15:54:53 -07:00
async function getBoxUpdate ( options ) {
2020-05-06 16:50:27 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2017-04-13 01:31:22 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token ,
boxVersion : constants . VERSION ,
automatic : options . automatic
} ;
2017-04-13 01:31:22 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/boxupdate ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-05-06 16:50:27 -07:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2021-08-18 15:54:53 -07:00
if ( response . status === 204 ) return ; // no update
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 || ! response . body ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
const updateInfo = response . body ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
if ( ! semver . valid ( updateInfo . version ) || semver . gt ( constants . VERSION , updateInfo . version ) ) {
2023-07-01 13:34:58 +05:30
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Update version invalid or is a downgrade: ${ response . status } ${ response . text } ` ) ;
2021-08-18 15:54:53 -07:00
}
2017-04-13 01:31:22 -07:00
2021-08-18 15:54:53 -07:00
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
2023-06-26 18:06:37 +05:30
if ( ! updateInfo . version || typeof updateInfo . version !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad version): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . changelog || ! Array . isArray ( updateInfo . changelog ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad changelog): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . sourceTarballUrl || typeof updateInfo . sourceTarballUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad sourceTarballUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . sourceTarballSigUrl || typeof updateInfo . sourceTarballSigUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad sourceTarballSigUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . boxVersionsUrl || typeof updateInfo . boxVersionsUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad boxVersionsUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . boxVersionsSigUrl || typeof updateInfo . boxVersionsSigUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad boxVersionsSigUrl): ${ response . status } ${ response . text } ` ) ;
2024-04-16 19:19:07 +02:00
if ( typeof updateInfo . unstable !== 'boolean' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad unstable): ${ response . status } ${ response . text } ` ) ;
2019-02-20 16:18:47 -08:00
2021-08-18 15:54:53 -07:00
return updateInfo ;
2017-04-13 01:31:22 -07:00
}
2021-08-18 15:54:53 -07:00
async function getAppUpdate ( app , options ) {
2017-04-13 01:23:11 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-05-06 16:50:27 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2017-04-13 01:23:11 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token ,
boxVersion : constants . VERSION ,
appId : app . appStoreId ,
appVersion : app . manifest . version ,
automatic : options . automatic
} ;
2017-04-13 01:23:11 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/appupdate ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2017-04-13 01:23:11 -07:00
2021-08-18 15:54:53 -07:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2021-08-18 15:54:53 -07:00
if ( response . status === 204 ) return ; // no update
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 || ! response . body ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
const updateInfo = response . body ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver . prerelease ( app . manifest . version ) ? app . manifest . version : ` ${ app . manifest . version } -0 ` ;
2018-03-01 11:36:39 -08:00
2021-08-18 15:54:53 -07:00
// do some sanity checks
if ( ! safe . query ( updateInfo , 'manifest.version' ) || semver . gt ( curAppVersion , safe . query ( updateInfo , 'manifest.version' ) ) ) {
debug ( 'Skipping malformed update of app %s version: %s. got %j' , app . id , curAppVersion , updateInfo ) ;
2023-06-26 18:06:37 +05:30
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Malformed update: ${ response . status } ${ response . text } ` ) ;
2021-08-18 15:54:53 -07:00
}
2017-04-13 01:23:11 -07:00
2021-08-18 15:54:53 -07:00
updateInfo . unstable = ! ! updateInfo . unstable ;
2020-06-05 16:09:12 -07:00
2021-08-18 15:54:53 -07:00
// { id, creationDate, manifest, unstable }
return updateInfo ;
2017-04-13 01:23:11 -07:00
}
2017-09-19 21:07:59 +02:00
2021-08-18 15:54:53 -07:00
async function registerCloudron ( data ) {
2019-05-07 14:25:27 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2019-02-25 17:29:42 -08:00
2023-12-02 11:23:03 +01:00
const { domain , setupToken , accessToken , version , existingApps } = data ;
2019-02-25 17:29:42 -08:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/register_cloudron ` )
2023-12-02 11:23:03 +01:00
. send ( { domain , setupToken , accessToken , version , existingApps } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2019-02-25 17:29:42 -08:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2023-12-02 16:42:03 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Setup token invalid' ) ;
2021-08-18 15:54:53 -07:00
if ( response . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to register cloudron: ${ response . statusCode } ${ error . message } ` ) ;
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07:00
if ( ! response . body . cloudronId ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response - no cloudron id' ) ;
if ( ! response . body . cloudronToken ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response - no token' ) ;
2019-05-03 16:27:47 -07:00
2023-08-02 21:49:29 +05:30
await settings . set ( settings . CLOUDRON _ID _KEY , response . body . cloudronId ) ;
2023-08-02 20:07:03 +05:30
await settings . set ( settings . APPSTORE _API _TOKEN _KEY , response . body . cloudronToken ) ;
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07:00
debug ( ` registerCloudron: Cloudron registered with id ${ response . body . cloudronId } ` ) ;
2024-02-26 11:37:23 +01:00
// app could already have been installed if we deleted the cloudron.io record and user re-registers
for ( const app of await apps . list ( ) ) {
await purchaseApp ( { appId : app . id , appstoreId : app . appStoreId , manifestId : app . manifest . id || 'customapp' } ) ;
}
2019-05-03 16:27:47 -07:00
}
2021-08-18 15:54:53 -07:00
async function updateCloudron ( data ) {
2021-04-13 14:19:45 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2022-03-31 23:27:00 -07:00
const { domain } = data ;
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token
} ;
2021-04-13 14:19:45 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/update_cloudron ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2022-03-31 23:27:00 -07:00
. send ( { domain } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2021-04-13 14:19:45 -07:00
2021-08-18 15:54:53 -07:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2021-04-13 14:19:45 -07:00
2021-08-18 15:54:53 -07:00
debug ( ` updateCloudron: Cloudron updated with data ${ JSON . stringify ( data ) } ` ) ;
2021-04-13 14:19:45 -07:00
}
2023-12-02 11:23:03 +01:00
async function registerCloudronWithSetupToken ( options ) {
2019-05-03 16:27:47 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2023-12-02 11:23:03 +01:00
const { domain } = await dashboard . getLocation ( ) ;
2021-08-25 15:18:58 -07:00
2023-12-02 11:23:03 +01:00
await registerCloudron ( { domain , setupToken : options . setupToken , version : constants . VERSION } ) ;
}
async function registerCloudronWithLogin ( options ) {
assert . strictEqual ( typeof options , 'object' ) ;
if ( options . signup ) await registerUser ( options . email , options . password ) ;
2021-08-18 15:54:53 -07:00
const result = await login ( options . email , options . password , options . totpToken || '' ) ;
2023-12-02 11:23:03 +01:00
2023-08-11 19:41:05 +05:30
const { domain } = await dashboard . getLocation ( ) ;
2023-12-02 11:23:03 +01:00
2023-08-11 19:41:05 +05:30
await registerCloudron ( { domain , accessToken : result . accessToken , version : constants . VERSION } ) ;
2019-02-25 17:29:42 -08:00
}
2022-04-04 13:54:57 -07:00
async function unregister ( ) {
2023-08-02 21:49:29 +05:30
await settings . set ( settings . CLOUDRON _ID _KEY , '' ) ;
2023-08-02 20:07:03 +05:30
await settings . set ( settings . APPSTORE _API _TOKEN _KEY , '' ) ;
2022-04-04 13:54:57 -07:00
}
2021-08-18 15:54:53 -07:00
async function createTicket ( info , auditSource ) {
2017-11-14 20:34:25 -08:00
assert . strictEqual ( typeof info , 'object' ) ;
assert . strictEqual ( typeof info . email , 'string' ) ;
assert . strictEqual ( typeof info . displayName , 'string' ) ;
assert . strictEqual ( typeof info . type , 'string' ) ;
assert . strictEqual ( typeof info . subject , 'string' ) ;
assert . strictEqual ( typeof info . description , 'string' ) ;
2019-12-16 14:06:55 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-10-06 16:01:49 +02:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2020-10-06 16:01:49 +02:00
2021-08-18 15:54:53 -07:00
if ( info . enableSshSupport ) {
await safe ( support . enableRemoteSupport ( true , auditSource ) ) ;
2023-08-03 13:38:42 +05:30
info . ipv4 = await network . getIPv4 ( ) ;
2020-10-06 16:01:49 +02:00
}
2021-08-20 09:19:44 -07:00
info . app = info . appId ? await apps . get ( info . appId ) : null ;
2021-08-18 15:54:53 -07:00
info . supportEmail = constants . SUPPORT _EMAIL ; // destination address for tickets
2017-11-14 20:34:25 -08:00
2023-08-04 15:34:38 +05:30
const request = superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/ticket ` )
2021-08-18 15:54:53 -07:00
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ;
2020-10-06 16:01:49 +02:00
2021-08-18 15:54:53 -07:00
// either send as JSON through body or as multipart, depending on attachments
if ( info . app ) {
request . field ( 'infoJSON' , JSON . stringify ( info ) ) ;
2017-11-14 20:34:25 -08:00
2022-01-04 09:12:45 -08:00
const logPaths = await apps . getLogPaths ( info . app ) ;
for ( const logPath of logPaths ) {
2024-10-15 10:10:15 +02:00
const [ error , logs ] = await safe ( shell . spawn ( 'tail' , [ '--lines=1000' , logPath ] , { encoding : 'utf8' } ) ) ;
2024-02-20 23:09:49 +01:00
if ( ! error && logs ) request . attach ( path . basename ( logPath ) , logs , path . basename ( logPath ) ) ;
2022-01-04 09:12:45 -08:00
}
2021-08-18 15:54:53 -07:00
} else {
request . send ( info ) ;
}
2020-10-06 17:53:04 +02:00
2021-08-18 15:54:53 -07:00
const [ error , response ] = await safe ( request ) ;
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2018-03-05 17:03:54 +01:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _SUPPORT _TICKET , auditSource , info ) ;
2019-12-16 14:06:55 -08:00
2021-08-18 15:54:53 -07:00
return { message : ` An email was sent to ${ constants . SUPPORT _EMAIL } . We will get back shortly! ` } ;
2017-11-14 20:34:25 -08:00
}
2019-05-04 11:45:03 -07:00
2023-08-04 15:34:38 +05:30
async function downloadManifest ( appStoreId , manifest ) {
if ( ! appStoreId && ! manifest ) throw new BoxError ( BoxError . BAD _FIELD , 'Neither manifest nor appStoreId provided' ) ;
if ( ! appStoreId ) return { appStoreId : '' , manifest } ;
2024-06-15 17:08:42 +02:00
const [ id , version ] = appStoreId . split ( '@' ) ;
if ( ! manifestFormat . isId ( id ) ) throw new BoxError ( BoxError . BAD _FIELD , 'appStoreId is not valid' ) ;
if ( version && ! semver . valid ( version ) ) throw new BoxError ( BoxError . BAD _FIELD , 'package version is not valid semver' ) ;
2023-08-04 15:34:38 +05:30
2024-06-15 17:08:42 +02:00
const url = await getApiServerOrigin ( ) + '/api/v1/apps/' + id + ( version ? '/versions/' + version : '' ) ;
2023-08-04 15:34:38 +05:30
debug ( ` downloading manifest from ${ url } ` ) ;
2024-04-23 11:41:49 +02:00
const [ error , response ] = await safe ( superagent . get ( url ) . timeout ( 60 * 1000 ) . ok ( ( ) => true ) ) ;
2023-08-04 15:34:38 +05:30
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Network error downloading manifest:' + error . message ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . NOT _FOUND , ` Failed to get app info from store. status: ${ response . status } text: ${ response . text } ` ) ;
if ( ! response . body . manifest || typeof response . body . manifest !== 'object' ) throw new BoxError ( BoxError . NOT _FOUND , ` Missing manifest. Failed to get app info from store. status: ${ response . status } text: ${ response . text } ` ) ;
2024-06-15 17:08:42 +02:00
return { appStoreId : id , manifest : response . body . manifest } ;
2023-08-04 15:34:38 +05:30
}
2023-04-02 18:03:41 +02:00
async function getApps ( ) {
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2019-05-04 11:45:03 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/apps ` )
2023-08-02 19:28:14 +05:30
. query ( { accessToken : token , boxVersion : constants . VERSION , unstable : true } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-02-05 11:58:10 -08:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 403 || response . status === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App listing failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
if ( ! response . body . apps ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2020-02-05 11:58:10 -08:00
2023-08-02 20:18:00 +05:30
return response . body . apps ;
2019-05-04 11:45:03 -07:00
}
2021-08-18 15:54:53 -07:00
async function getAppVersion ( appId , version ) {
2019-05-04 11:45:03 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof version , 'string' ) ;
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2020-02-05 11:58:10 -08:00
2023-08-04 15:34:38 +05:30
let url = ` ${ await getApiServerOrigin ( ) } /api/v1/apps/ ${ appId } ` ;
2021-08-18 15:54:53 -07:00
if ( version !== 'latest' ) url += ` /versions/ ${ version } ` ;
2019-05-04 11:45:03 -07:00
2021-08-18 15:54:53 -07:00
const [ error , response ] = await safe ( superagent . get ( url )
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-02-05 11:58:10 -08:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2024-10-30 17:59:55 +01:00
if ( response . status === 403 || response . statusCode === 401 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid appstore token' ) ;
2024-10-30 16:21:21 +01:00
if ( response . status === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Could not find app ${ appId } ` ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App fetch failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2021-08-18 15:54:53 -07:00
return response . body ;
2019-05-04 11:45:03 -07:00
}
2021-08-18 15:54:53 -07:00
async function getApp ( appId ) {
2019-05-04 11:45:03 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2021-08-18 15:54:53 -07:00
return await getAppVersion ( appId , 'latest' ) ;
2019-05-04 11:45:03 -07:00
}
2023-08-04 15:34:38 +05:30
async function downloadIcon ( appStoreId , version ) {
const iconUrl = ` ${ await getApiServerOrigin ( ) } /api/v1/apps/ ${ appStoreId } /versions/ ${ version } /icon ` ;
return await promiseRetry ( { times : 10 , interval : 5000 , debug } , async function ( ) {
const [ networkError , response ] = await safe ( superagent . get ( iconUrl )
. buffer ( true )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2023-08-04 15:34:38 +05:30
. ok ( ( ) => true ) ) ;
2024-10-30 18:30:47 +01:00
if ( networkError ) throw new BoxError ( BoxError . NETWORK _ERROR , Object . assign ( networkError , { message : 'Downloading icon' } ) ) ;
2023-08-04 15:34:38 +05:30
if ( response . status !== 200 ) return ; // ignore error. this can also happen for apps installed with cloudron-cli
2024-06-15 17:23:20 +02:00
const contentType = response . headers [ 'content-type' ] ;
if ( ! contentType || contentType . indexOf ( 'image' ) === - 1 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'AppStore returned invalid icon for app' ) ;
2023-08-04 15:34:38 +05:30
return response . body ;
} ) ;
}