2023-07-14 14:48:43 +02:00
2025-03-03 11:22:56 +01:00
import { API _ORIGIN , APP _TYPES , PROXY _APP _ID , HSTATES , ISTATES , RSTATES } from '../constants.js' ;
2025-03-18 19:04:47 +01:00
import { eachLimit } from 'async' ;
2025-07-10 11:55:11 +02:00
import { fetcher } from '@cloudron/pankow' ;
import { sleep } from '@cloudron/pankow/utils' ;
2025-03-16 11:12:49 +01:00
import moment from 'moment-timezone' ;
2025-03-18 19:04:47 +01:00
import DashboardModel from './DashboardModel.js' ;
import ProfileModel from './ProfileModel.js' ;
const dashboardModel = DashboardModel . create ( ) ;
const profileModel = ProfileModel . create ( ) ;
2025-01-02 19:04:07 +01:00
function installationStateLabel ( app ) {
if ( ! app ) return '' ;
const waiting = app . progress === 0 ? ' (Queued)' : '' ;
switch ( app . installationState ) {
case ISTATES . PENDING _INSTALL :
return 'Installing' + waiting ;
case ISTATES . PENDING _CLONE :
return 'Cloning' + waiting ;
case ISTATES . PENDING _LOCATION _CHANGE :
case ISTATES . PENDING _CONFIGURE :
case ISTATES . PENDING _RECREATE _CONTAINER :
case ISTATES . PENDING _SERVICES _CHANGE :
case ISTATES . PENDING _DEBUG :
return 'Configuring' + waiting ;
case ISTATES . PENDING _RESIZE :
return 'Resizing' + waiting ;
case ISTATES . PENDING _DATA _DIR _MIGRATION :
return 'Migrating data' + waiting ;
case ISTATES . PENDING _UNINSTALL : return 'Uninstalling' + waiting ;
case ISTATES . PENDING _RESTORE : return 'Restoring' + waiting ;
case ISTATES . PENDING _IMPORT : return 'Importing' + waiting ;
case ISTATES . PENDING _UPDATE : return 'Updating' + waiting ;
case ISTATES . PENDING _BACKUP : return 'Backing up' + waiting ;
case ISTATES . PENDING _START : return 'Starting' + waiting ;
case ISTATES . PENDING _STOP : return 'Stopping' + waiting ;
case ISTATES . PENDING _RESTART : return 'Restarting' + waiting ;
case ISTATES . ERROR : {
if ( app . error && app . error . message === 'ETRYAGAIN' ) return 'DNS Error' ;
return 'Error' ;
}
case ISTATES . INSTALLED : {
if ( app . debugMode ) {
return 'Recovery Mode' ;
} else if ( app . runState === RSTATES . RUNNING ) {
if ( ! app . health ) return 'Starting...' ; // no data yet
if ( app . type === APP _TYPES . LINK ) return '' ;
if ( app . health === HSTATES . HEALTHY ) return 'Running' ;
return 'Not responding' ; // dead/exit/unhealthy
} else if ( app . runState === RSTATES . STOPPED ) {
return 'Stopped' ;
} else {
return app . runState ;
}
}
default : return app . installationState ;
}
}
function installationActive ( app ) {
if ( app . installationState === ISTATES . ERROR ) return false ;
if ( app . installationState === ISTATES . INSTALLED ) return false ;
return true ;
}
function appProgressMessage ( app ) {
return app . message || ( app . error ? app . error . message : '' ) ;
}
2025-05-20 10:39:37 +02:00
function pendingChecklistItems ( app ) {
if ( ! app . checklist ) return 0 ;
return Object . keys ( app . checklist ) . filter ( function ( key ) { return ! app . checklist [ key ] . acknowledged ; } ) . length ;
}
2025-01-31 21:02:48 +01:00
function create ( ) {
const accessToken = localStorage . token ;
2025-03-18 19:04:47 +01:00
let config = null ;
let profile = null ;
async function loadConfigAndProfile ( ) {
let [ error , result ] = await dashboardModel . config ( ) ;
if ( error ) return console . error ( error ) ;
config = result ;
[ error , result ] = await profileModel . get ( ) ;
if ( error ) return console . error ( error ) ;
profile = result ;
}
2025-01-02 19:04:07 +01:00
async function getTask ( appId ) {
let error , result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ appId } /task ` , { access _token : accessToken } ) ;
2025-01-02 19:04:07 +01:00
} catch ( e ) {
error = e ;
}
if ( error || result . status !== 200 ) {
console . error ( 'Failed to get task for app.' , error || result . status ) ;
return null ;
}
return result . body ;
}
2023-07-14 14:48:43 +02:00
2025-03-18 19:04:47 +01:00
async function postProcess ( app ) {
if ( ! profile || ! config ) await loadConfigAndProfile ( ) ;
app . ssoAuth = app . sso && ( app . manifest . addons [ 'ldap' ] || app . manifest . addons [ 'oidc' ] || app . manifest . addons [ 'proxyAuth' ] ) ; // checking app.sso first ensures app.manifest.addons is not null
app . type = app . manifest . id === PROXY _APP _ID ? APP _TYPES . PROXIED : APP _TYPES . APP ;
app . iconUrl = app . iconUrl ? ` ${ API _ORIGIN } ${ app . iconUrl } ?ts= ${ new Date ( app . ts ) . getTime ( ) } ` : ` ${ API _ORIGIN } /img/appicon_fallback.png ` ; // calculate full icon url with cache busting
// only fetch if we have permissions and a taskId is set/active
if ( ! app . taskId || ( app . accessLevel !== 'operator' && app . accessLevel !== 'admin' ) ) {
app . progress = 0 ;
app . message = '' ;
app . taskMinutesActive = 0 ;
} else {
const task = await getTask ( app . id ) ;
if ( task ) {
2025-07-07 17:37:48 +02:00
app . progress = task . percent || 5 ; // avoid starting with empty
2025-03-18 19:04:47 +01:00
app . message = task . message ;
app . taskMinutesActive = moment . duration ( moment . utc ( ) . diff ( moment . utc ( task . creationTime ) ) ) . asMinutes ( ) ;
} else {
app . progress = 0 ;
app . message = '' ;
app . taskMinutesActive = 0 ;
}
}
2025-05-15 15:04:13 +02:00
function expandTemplateVars ( text ) {
2025-03-18 19:04:47 +01:00
// we chose - because underscore has special meaning in markdown
text = text . replace ( /\$CLOUDRON-APP-LOCATION/g , app . subdomain ) ;
text = text . replace ( /\$CLOUDRON-APP-DOMAIN/g , app . domain ) ;
text = text . replace ( /\$CLOUDRON-APP-FQDN/g , app . fqdn ) ;
text = text . replace ( /\$CLOUDRON-APP-ORIGIN/g , 'https://' + app . fqdn ) ;
text = text . replace ( /\$CLOUDRON-API-DOMAIN/g , config . adminFqdn ) ;
text = text . replace ( /\$CLOUDRON-API-ORIGIN/g , 'https://' + config . adminFqdn ) ;
text = text . replace ( /\$CLOUDRON-USERNAME/g , profile . username ) ;
text = text . replace ( /\$CLOUDRON-APP-ID/g , app . id ) ;
2025-05-15 15:04:13 +02:00
return text ;
}
if ( app . manifest . postInstallMessage ) {
let text = expandTemplateVars ( app . manifest . postInstallMessage ) ;
2025-03-18 19:04:47 +01:00
// [^] matches even newlines. '?' makes it non-greedy
if ( app . sso ) text = text . replace ( /<nosso>[^]*?<\/nosso>/g , '' ) ;
else text = text . replace ( /<sso>[^]*?<\/sso>/g , '' ) ;
2025-03-18 19:20:32 +01:00
app . manifest . postInstallMessage = text . trim ( ) ;
2025-03-18 19:04:47 +01:00
}
2025-06-02 22:20:51 +02:00
// only admins have this property
if ( app . checklist ) {
for ( const key of Object . keys ( app . checklist ) ) {
app . checklist [ key ] . message = expandTemplateVars ( app . checklist [ key ] . message ) ;
}
} else {
app . checklist = { } ;
2025-05-15 15:04:13 +02:00
}
2025-03-18 19:04:47 +01:00
return app ;
}
2023-07-14 14:48:43 +02:00
return {
2024-12-29 00:36:48 +01:00
name : 'AppsModel' ,
2025-01-02 19:04:07 +01:00
getTask ,
2025-02-21 14:07:07 +01:00
isStopped ( app ) {
if ( app . installationState === ISTATES . PENDING _START || app . installationState === ISTATES . PENDING _STOP ) {
return app . installationState === ISTATES . PENDING _START ;
} else {
return app . runState === RSTATES . STOPPED ;
}
} ,
2025-01-06 14:35:14 +01:00
async install ( manifest , config ) {
const data = {
appStoreId : manifest . id + '@' + manifest . version ,
subdomain : config . subdomain ,
domain : config . domain ,
secondaryDomains : config . secondaryDomains ,
ports : config . ports ,
accessRestriction : config . accessRestriction ,
cert : config . cert ,
key : config . key ,
sso : config . sso ,
overwriteDns : config . overwriteDns ,
upstreamUri : config . upstreamUri ,
backupId : config . backupId // when restoring from archive
} ;
2025-03-21 10:55:55 +01:00
let result ;
2025-01-06 14:35:14 +01:00
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps ` , data , { access _token : accessToken } ) ;
2025-01-06 14:35:14 +01:00
} catch ( e ) {
2025-03-21 10:55:55 +01:00
return [ e ] ;
2025-01-06 14:35:14 +01:00
}
2025-03-21 10:55:55 +01:00
if ( result . status !== 202 ) return [ result ] ;
2025-01-06 14:35:14 +01:00
2025-04-23 15:32:42 +02:00
return [ null , result . body ] ;
2025-01-06 14:35:14 +01:00
} ,
2024-12-29 00:36:48 +01:00
async list ( ) {
let error , result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps ` , { access _token : accessToken } ) ;
2024-12-29 00:36:48 +01:00
} catch ( e ) {
error = e ;
}
2025-01-25 17:09:53 +01:00
if ( error || result . status !== 200 ) return [ error || result ] ;
2024-12-29 00:36:48 +01:00
2025-03-18 19:04:47 +01:00
await eachLimit ( result . body . apps , 10 , postProcess ) ;
2025-01-02 19:04:07 +01:00
2025-01-25 17:09:53 +01:00
return [ null , result . body . apps ] ;
2024-12-29 00:36:48 +01:00
} ,
2025-01-21 16:54:56 +01:00
async get ( id ) {
2023-07-14 14:48:43 +02:00
let error , result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } ` , { access _token : accessToken } ) ;
2023-07-14 14:48:43 +02:00
} catch ( e ) {
error = e ;
}
2025-01-21 16:54:56 +01:00
if ( error || result . status !== 200 ) return [ error || result ] ;
2025-02-25 15:21:32 +01:00
2025-03-18 19:04:47 +01:00
return [ null , await postProcess ( result . body ) ] ;
2023-07-14 14:48:43 +02:00
} ,
2025-01-27 12:18:15 +01:00
async restart ( id ) {
2025-02-22 18:31:21 +01:00
let result ;
2023-07-14 14:48:43 +02:00
try {
2025-06-11 11:43:58 +02:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /restart ` , { } , { access _token : accessToken } ) ;
2023-07-14 14:48:43 +02:00
} catch ( e ) {
2025-02-22 18:31:21 +01:00
return [ e ] ;
2023-07-14 14:48:43 +02:00
}
2025-02-22 18:31:21 +01:00
if ( result . status !== 202 ) return [ result ] ;
2023-07-14 14:48:43 +02:00
while ( true ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } ` , { access _token : accessToken } ) ;
2023-07-14 14:48:43 +02:00
} catch ( e ) {
2025-02-22 18:31:21 +01:00
return [ e ] ;
2023-07-14 14:48:43 +02:00
}
2025-02-22 18:31:21 +01:00
if ( result . status !== 200 ) return [ result ] ;
2023-07-14 14:48:43 +02:00
2025-02-22 18:31:21 +01:00
// are we done here?
if ( result . body . installationState !== ISTATES . INSTALLED ) await sleep ( 2000 ) ;
else return [ null ] ;
2023-07-14 14:48:43 +02:00
}
2025-02-20 16:12:36 +01:00
} ,
2025-02-21 12:20:23 +01:00
async start ( id ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /start ` , { } , { access _token : accessToken } ) ;
2025-02-21 12:20:23 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null , result . body . taskId ] ;
} ,
async stop ( id ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /stop ` , { } , { access _token : accessToken } ) ;
2025-02-21 12:20:23 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null , result . body . taskId ] ;
} ,
2025-02-20 16:12:36 +01:00
async configure ( id , setting , data ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /configure/ ${ setting } ` , data , { access _token : accessToken } ) ;
2025-02-20 16:12:36 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 && result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
2025-02-21 14:07:07 +01:00
async uninstall ( id ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /uninstall ` , { } , { access _token : accessToken } ) ;
2025-02-21 14:07:07 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
2025-04-22 18:03:01 +02:00
async archive ( id , backupId ) {
let result ;
try {
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /archive ` , { backupId } , { access _token : accessToken } ) ;
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
2025-02-21 16:30:59 +01:00
async getEvents ( id ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /eventlog ` , { page : 1 , per _page : 100 , access _token : accessToken } ) ;
2025-02-21 16:30:59 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 ) return [ result ] ;
return [ null , result . body . eventlogs ] ;
} ,
2025-06-26 16:37:51 +02:00
async checkUpdate ( id ) {
2025-02-21 20:58:43 +01:00
let result ;
try {
2025-06-26 16:37:51 +02:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /check_update ` , { } , { access _token : accessToken } ) ;
2025-02-21 20:58:43 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 ) return [ result ] ;
return [ null , result . body . update ] ;
} ,
async update ( id , manifest , skipBackup = false ) {
const data = {
appStoreId : ` ${ manifest . id } @ ${ manifest . version } ` ,
skipBackup : ! ! skipBackup ,
} ;
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /update ` , data , { access _token : accessToken } ) ;
2025-02-21 20:58:43 +01:00
} catch ( e ) {
return [ e ] ;
}
2025-02-26 17:22:28 +01:00
if ( result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
async backups ( id ) {
// we fetch probably enough to avoid pagination
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /backups ` , { page : 1 , per _page : 100 , access _token : accessToken } ) ;
2025-02-26 17:22:28 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 ) return [ result ] ;
return [ null , result . body . backups ] ;
} ,
async backup ( id ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /backup ` , { } , { access _token : accessToken } ) ;
2025-02-26 17:22:28 +01:00
} catch ( e ) {
return [ e ] ;
}
2025-02-21 20:58:43 +01:00
if ( result . status !== 202 ) return [ result ] ;
2025-04-22 17:56:50 +02:00
return [ null , result . body . taskId ] ;
2025-02-21 20:58:43 +01:00
} ,
2025-02-26 20:30:33 +01:00
async updateBackup ( id , backupId , label , preserveSecs ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /backups/ ${ backupId } ` , { label , preserveSecs } , { access _token : accessToken } ) ;
2025-02-26 20:30:33 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 ) return [ result ] ;
return [ null ] ;
} ,
2025-02-27 17:09:18 +01:00
async restore ( id , backupId ) {
let result ;
try {
2025-03-03 11:22:56 +01:00
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /restore ` , { backupId } , { access _token : accessToken } ) ;
2025-02-27 17:09:18 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
2025-05-21 16:26:36 +02:00
async getMetrics ( id , options ) {
2025-03-07 11:54:43 +01:00
let result ;
try {
2025-05-21 16:26:36 +02:00
result = await fetcher . get ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /metrics ` , { fromSecs : options . fromSecs , intervalSecs : options . intervalSecs , access _token : accessToken } ) ;
2025-03-07 11:54:43 +01:00
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 ) return [ result ] ;
return [ null , result . body ] ;
} ,
2025-07-03 19:01:40 +02:00
async getMetricStream ( id ) {
return new EventSource ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /metricstream?access_token= ${ accessToken } ` ) ;
2025-07-03 16:39:04 +02:00
} ,
2025-03-22 11:19:06 +01:00
async repair ( id , data ) {
let result ;
try {
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /repair ` , data , { access _token : accessToken } ) ;
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 200 && result . status !== 202 ) return [ result ] ;
return [ null , result . body ] ;
} ,
2025-03-22 11:59:27 +01:00
async ackChecklistItem ( id , key , done ) {
let result ;
try {
result = await fetcher . put ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /checklist/ ${ key } ` , { done } , { access _token : accessToken } ) ;
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null ] ;
} ,
2025-04-25 00:00:06 +02:00
async clone ( id , config ) {
const data = {
subdomain : config . subdomain ,
domain : config . domain ,
secondaryDomains : config . secondaryDomains ,
ports : config . ports ,
backupId : config . backupId ,
overwriteDns : ! ! config . overwriteDns
} ;
let result ;
try {
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /clone ` , data , { access _token : accessToken } ) ;
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 201 ) return [ result ] ;
return [ null , result . body ] ;
} ,
2025-04-29 16:58:36 +02:00
async import ( id , remotePath , backupFormat , backupConfig ) {
let result ;
try {
result = await fetcher . post ( ` ${ API _ORIGIN } /api/v1/apps/ ${ id } /import ` , { remotePath , backupFormat , backupConfig } , { access _token : accessToken } ) ;
} catch ( e ) {
return [ e ] ;
}
if ( result . status !== 202 ) return [ result ] ;
return [ null , result . body ] ;
} ,
2023-07-14 14:48:43 +02:00
} ;
}
export default {
2025-01-02 19:04:07 +01:00
create ,
installationStateLabel ,
installationActive ,
appProgressMessage ,
2025-05-20 10:39:37 +02:00
pendingChecklistItems ,
2023-07-14 14:48:43 +02:00
} ;