'use strict'; /* global $ */ /* global angular */ /* global EventSource */ /* global async */ // keep in sync with box/src/apps.js var ISTATES = { PENDING_INSTALL: 'pending_install', PENDING_CLONE: 'pending_clone', PENDING_CONFIGURE: 'pending_configure', PENDING_UNINSTALL: 'pending_uninstall', PENDING_RESTORE: 'pending_restore', PENDING_IMPORT: 'pending_import', PENDING_UPDATE: 'pending_update', PENDING_BACKUP: 'pending_backup', PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change PENDING_LOCATION_CHANGE: 'pending_location_change', PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration', PENDING_RESIZE: 'pending_resize', PENDING_DEBUG: 'pending_debug', PENDING_START: 'pending_start', PENDING_STOP: 'pending_stop', PENDING_RESTART: 'pending_restart', ERROR: 'error', INSTALLED: 'installed' }; var HSTATES = { HEALTHY: 'healthy', UNHEALTHY: 'unhealthy', ERROR: 'error', DEAD: 'dead' }; var RSTATES ={ RUNNING: 'running', STOPPED: 'stopped' }; var ERROR = { ACCESS_DENIED: 'Access Denied', ALREADY_EXISTS: 'Already Exists', BAD_FIELD: 'Bad Field', COLLECTD_ERROR: 'Collectd Error', CONFLICT: 'Conflict', DATABASE_ERROR: 'Database Error', DNS_ERROR: 'DNS Error', DOCKER_ERROR: 'Docker Error', EXTERNAL_ERROR: 'External Error', FS_ERROR: 'FileSystem Error', INTERNAL_ERROR: 'Internal Error', LOGROTATE_ERROR: 'Logrotate Error', NETWORK_ERROR: 'Network Error', NOT_FOUND: 'Not found', REVERSEPROXY_ERROR: 'ReverseProxy Error', TASK_ERROR: 'Task Error', UNKNOWN_ERROR: 'Unknown Error' // only used for portin, }; var ROLES = { OWNER: 'owner', ADMIN: 'admin', USER_MANAGER: 'usermanager', USER: 'user' }; // sync up with tasks.js var TASK_TYPES = { TASK_APP: 'app', TASK_BACKUP: 'backup', TASK_UPDATE: 'update', TASK_RENEW_CERTS: 'renewcerts', TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert', TASK_CLEAN_BACKUPS: 'cleanBackups', TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', TASK_SYNC_DNS_RECORDS: 'syncDnsRecords', }; var SECRET_PLACEHOLDER = String.fromCharCode(0x25CF).repeat(8); // ---------------------------------------------- // Helper to ensure loading a fallback app icon on first load failure // ---------------------------------------------- function imageErrorHandler(elem) { elem.src = elem.getAttribute('fallback-icon'); elem.onerror = null; // avoid retry after default icon cannot be loaded } // ---------------------------------------------- // Shared Angular Filters // ---------------------------------------------- // binary units (non SI) 1024 based function prettyByteSize(size, fallback) { if (!size) return fallback || 0; var i = Math.floor(Math.log(size) / Math.log(1024)); return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; } angular.module('Application').filter('prettyByteSize', function () { return function (size, fallback) { return prettyByteSize(size, fallback) || '0 kb'; }; }); angular.module('Application').filter('prettyDiskSize', function () { return function (size, fallback) { return prettyByteSize(size, fallback) || 'Not available yet'; }; }); angular.module('Application').filter('trKeyFromPeriod', function () { return function (period) { if (period === 6) return 'app.graphs.period.6h'; if (period === 12) return 'app.graphs.period.12h'; if (period === 24) return 'app.graphs.period.24h'; if (period === 24*7) return 'app.graphs.period.7d'; if (period === 24*30) return 'app.graphs.period.30d'; return ''; }; }); angular.module('Application').filter('prettyDate', function ($translate) { // http://ejohn.org/files/pretty.js return function prettyDate(utc) { var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone! diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff < 0) return $translate.instant('main.prettyDate.justNow', {}); return day_diff === 0 && ( diff < 60 && $translate.instant('main.prettyDate.justNow', {}) || diff < 120 && $translate.instant('main.prettyDate.minutesAgo', { m: 1 }) || diff < 3600 && $translate.instant('main.prettyDate.minutesAgo', { m: Math.floor( diff / 60 ) }) || diff < 7200 && $translate.instant('main.prettyDate.hoursAgo', { h: 1 }) || diff < 86400 && $translate.instant('main.prettyDate.hoursAgo', { h: Math.floor( diff / 3600 ) }) ) || day_diff === 1 && $translate.instant('main.prettyDate.yeserday', {}) || day_diff < 7 && $translate.instant('main.prettyDate.daysAgo', { d: day_diff }) || day_diff < 31 && $translate.instant('main.prettyDate.weeksAgo', { w: Math.ceil( day_diff / 7 ) }) || day_diff < 365 && $translate.instant('main.prettyDate.monthsAgo', { m: Math.round( day_diff / 30 ) }) || $translate.instant('main.prettyDate.yearsAgo', { m: Math.round( day_diff / 365 ) }); }; }); angular.module('Application').filter('prettyLongDate', function () { return function prettyLongDate(utc) { return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone! }; }); angular.module('Application').filter('prettyShortDate', function () { return function prettyShortDate(utc) { return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone! }; }); angular.module('Application').filter('markdown2html', function () { var converter = new showdown.Converter({ simplifiedAutoLink: true, strikethrough: true, tables: true, openLinksInNewWindow: true }); // without this cache, the code runs into some infinite loop (https://github.com/angular/angular.js/issues/3980) var cache = {}; return function (text) { if (cache[text]) return cache[text]; cache[text] = converter.makeHtml(text); return cache[text]; }; }); angular.module('Application').config(['$translateProvider', function ($translateProvider) { $translateProvider.useStaticFilesLoader({ prefix: 'translation/', suffix: '.json?' + '<%= revision %>' }); $translateProvider.useLocalStorage(); $translateProvider.preferredLanguage('en'); $translateProvider.fallbackLanguage('en'); }]); // Add shorthand "tr" filter to avoid having ot use "translate" // This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js // If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required function translateFilterFactory($parse, $translate) { var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) { if (!angular.isObject(interpolateParams)) { var ctx = this || { '__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f' }; interpolateParams = $parse(interpolateParams)(ctx); } return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage); }; if ($translate.statefulFilter()) { translateFilter.$stateful = true; } return translateFilter; } translateFilterFactory.displayName = 'translateFilterFactory'; angular.module('Application').filter('tr', translateFilterFactory); // ---------------------------------------------- // Cloudron REST API wrapper // ---------------------------------------------- angular.module('Application').service('Client', ['$http', '$interval', '$timeout', 'md5', 'Notification', function ($http, $interval, $timeout, md5, Notification) { var client = null; // variable available only here to avoid this._property pattern var token = null; function ClientError(statusCode, messageOrObject) { Error.call(this); this.name = this.constructor.name; this.statusCode = statusCode; if (messageOrObject === null || typeof messageOrObject === 'undefined') { this.message = 'Empty message or object'; } else if (typeof messageOrObject === 'string') { this.message = messageOrObject; } else if (messageOrObject) { angular.extend(this, messageOrObject); // status, message, reason and other properties } } function defaultErrorHandler(callback) { function handleServerOffline() { if (client.offline) return; client.offline = true; (function onlineCheck() { $http.get(client.apiOrigin + '/api/v1/cloudron/status', {}).success(function (data, status) { client.offline = false; client._reconnectListener.forEach(function (handler) { handler(); }); }).error(function (data, status) { $timeout(onlineCheck, 5000); }); })(); } return function (data, status) { // handle request killed by browser (eg. cors issue) if (data === null && status === -1) { handleServerOffline(); return callback(new ClientError('Request cancelled by browser')); } // re-login will make the code get a new token if (status === 401) return client.login(); if (status === 500 || status === 501) { // actual internal server error, most likely a bug or timeout log to console only to not alert the user if (!client.offline) { console.error(status, data); console.log('------\nCloudron Internal Error\n\nIf you see this, please send a mail with above log to support@cloudron.io\n------\n'); } } else if (status === 502 || status === 503 || status === 504) { // This means the box service is not reachable. We just show offline banner for now } if (status >= 502) { handleServerOffline(); return callback(new ClientError(status, data)); } var obj = data; try { obj = JSON.parse(data); } catch (e) {} callback(new ClientError(status, obj)); }; } function defaultSuccessHandler(callback) { return function (data, status) { return callback(null, data, status); }; } // XHR wrapper to set the auth header function get(url, config, callback) { if (arguments.length !== 3) { console.error('GET', arguments); throw('Wrong number of arguments'); } config = config || {}; config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; return $http.get(client.apiOrigin + url, config) .success(defaultSuccessHandler(callback)) .error(defaultErrorHandler(callback)); } function head(url, config, callback) { if (arguments.length !== 3) { console.error('HEAD', arguments); throw('Wrong number of arguments'); } config = config || {}; config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; return $http.head(client.apiOrigin + url, config) .success(defaultSuccessHandler(callback)) .error(defaultErrorHandler(callback)); } function post(url, data, config, callback) { if (arguments.length !== 4) { console.error('POST', arguments); throw('Wrong number of arguments'); } data = data || {}; config = config || {}; config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; return $http.post(client.apiOrigin + url, data, config) .success(defaultSuccessHandler(callback)) .error(defaultErrorHandler(callback)); } function put(url, data, config, callback) { if (arguments.length !== 4) { console.error('PUT', arguments); throw('Wrong number of arguments'); } data = data || {}; config = config || {}; config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; return $http.put(client.apiOrigin + url, data, config) .success(defaultSuccessHandler(callback)) .error(defaultErrorHandler(callback)); } function del(url, config, callback) { if (arguments.length !== 3) { console.error('DEL', arguments); throw('Wrong number of arguments'); } config = config || {}; config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; return $http.delete(client.apiOrigin + url, config) .success(defaultSuccessHandler(callback)) .error(defaultErrorHandler(callback)); } function Client() { this.offline = false; this._ready = false; this._configListener = []; this._readyListener = []; this._reconnectListener = []; this._userInfo = { id: null, username: null, email: null, twoFactorAuthenticationEnabled: false, source: null, avatarUrl: null }; this._config = { apiServerOrigin: null, webServerOrigin: null, fqdn: null, ip: null, revision: null, update: { box: null, apps: null }, progress: {}, region: null, size: null }; this._installedApps = []; this._installedAppsById = {}; this._appTags = []; // window.location fallback for websocket connections which do not have relative uris this.apiOrigin = '<%= apiOrigin %>' || window.location.origin; this.avatar = ''; this._availableLanguages = ['en']; this._appstoreAppCache = []; this.resetAvatar(); this.setToken(localStorage.token); } Client.prototype.error = function (error, action) { var message = ''; console.error(error); if (typeof error === 'object') { message = error.message || error; } else { message = error; } // give more info in case the error was a request which failed with empty response body, // this happens mostly if the box crashes if (message === 'Empty message or object') { message = 'Got empty response. Click to check the server logs.'; action = action || '/logs.html?id=box'; } this.notify('Cloudron Error', message, true, 'error', action); }; // handles application startup errors, mostly only when dashboard is loaded and api endpoint is down Client.prototype.initError = function (error, initFunction) { console.error('Application startup error', error); $timeout(initFunction, 5000); // we will try to re-init the app }; Client.prototype.clearNotifications = function () { Notification.clearAll(); }; /* If `action` is a non-empty string, it will be treated as a url, if it is a function, that function will be exectued on click */ Client.prototype.notify = function (title, message, persistent, type, action) { var options = { title: title, message: message}; if (persistent) options.delay = 'never'; // any non Number means never timeout if (action) { options.onClick = function (/* params */) { // if action is a string, we assume it is a link if (typeof action === 'string' && action !== '') window.location = action; else if (typeof action === 'function') action(); else console.warn('Notification action is not supported.', action); }; } if (type === 'error') Notification.error(options); else if (type === 'success') Notification.success(options); else if (type === 'info') Notification.info(options); else if (type === 'warning') Notification.warning(options); else throw('Invalid notification type "' + type + '"'); }; Client.prototype.setReady = function () { if (this._ready) return; this._ready = true; this._readyListener.forEach(function (callback) { callback(); }); // clear the listeners, we only callback once! this._readyListener = []; }; Client.prototype.onReady = function (callback) { if (this._ready) callback(); else this._readyListener.push(callback); }; Client.prototype.onConfig = function (callback) { this._configListener.push(callback); if (this._config && this._config.apiServerOrigin) callback(this._config); }; Client.prototype.onReconnect = function (callback) { if (this._ready) callback(); else this._reconnectListener.push(callback); }; Client.prototype.resetAvatar = function () { this.avatar = this.apiOrigin + '/api/v1/cloudron/avatar?' + String(Math.random()).slice(2); var favicon = $('#favicon'); if (favicon) favicon.attr('href', this.avatar); }; Client.prototype.setUserInfo = function (userInfo) { // In order to keep the angular bindings alive, set each property individually this._userInfo.id = userInfo.id; this._userInfo.username = userInfo.username; this._userInfo.email = userInfo.email; this._userInfo.fallbackEmail = userInfo.fallbackEmail; this._userInfo.displayName = userInfo.displayName; this._userInfo.twoFactorAuthenticationEnabled = userInfo.twoFactorAuthenticationEnabled; this._userInfo.role = userInfo.role; this._userInfo.source = userInfo.source; this._userInfo.avatarUrl = userInfo.avatarUrl + '?s=128&default=mp&ts=' + Date.now(); // we add the timestamp to avoid caching this._userInfo.isAtLeastOwner = [ ROLES.OWNER ].indexOf(userInfo.role) !== -1; this._userInfo.isAtLeastAdmin = [ ROLES.OWNER, ROLES.ADMIN ].indexOf(userInfo.role) !== -1; this._userInfo.isAtLeastUserManager = [ ROLES.OWNER, ROLES.ADMIN, ROLES.USER_MANAGER ].indexOf(userInfo.role) !== -1; }; Client.prototype.setConfig = function (config) { var that = this; angular.copy(config, this._config); <% if (appstore.webOrigin) { -%> this._config.webServerOrigin = '<%= appstore.webOrigin %>'; <% } -%> <% if (appstore.ApiOrigin) { -%> this._config.apiServerOrigin = '<%= appstore.apiOrigin %>'; <% } -%> // => This is just for easier testing // this._config.features.userMaxCount = 5; // this._config.features.userRoles = false; // this._config.features.userGroups = false; // this._config.features.domainMaxCount = 1; // this._config.features.externalLdap = false; // this._config.features.privateDockerRegistry = false; // this._config.features.branding = true; // this._config.features.support = true; // this._config.features.directoryConfig = true; // this._config.features.mailboxMaxCount = 5; // this._config.features.emailPremium = false; this._configListener.forEach(function (callback) { callback(that._config); }); }; Client.prototype.getInstalledApps = function () { return this._installedApps; }; Client.prototype.getAppTags = function () { return this._appTags; }; Client.prototype.getUserInfo = function () { return this._userInfo; }; Client.prototype.getConfig = function () { return this._config; }; Client.prototype.getAvailableLanguages = function () { return this._availableLanguages; }; Client.prototype.setToken = function (accessToken) { if (!accessToken) localStorage.removeItem('token'); else localStorage.token = accessToken; // set the token closure token = accessToken; }; Client.prototype.getToken = function () { return token; }; Client.prototype.makeURL = function (url) { if (url.indexOf('?') === -1) { return this.apiOrigin + url + '?access_token=' + token; } else { return this.apiOrigin + url + '&access_token=' + token; } }; /* * Rest API wrappers */ Client.prototype.config = function (callback) { get('/api/v1/config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.userInfo = function (callback) { get('/api/v1/profile', null, function (error, data, status) { if (error) return callback(error); if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.changeCloudronAvatar = function (avatarFile, callback) { var fd = new FormData(); fd.append('avatar', avatarFile); var config = { headers: { 'Content-Type': undefined }, transformRequest: angular.identity }; post('/api/v1/branding/cloudron_avatar', fd, config, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.changeCloudronName = function (name, callback) { var data = { name: name }; post('/api/v1/branding/cloudron_name', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.installApp = function (id, manifest, title, config, callback) { var that = this; var data = { appStoreId: id + '@' + manifest.version, location: config.location, domain: config.domain, portBindings: config.portBindings, accessRestriction: config.accessRestriction, cert: config.cert, key: config.key, sso: config.sso, overwriteDns: config.overwriteDns }; post('/api/v1/apps/install', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.id); }); }; Client.prototype.cloneApp = function (appId, config, callback) { var data = { location: config.location, domain: config.domain, portBindings: config.portBindings, backupId: config.backupId }; post('/api/v1/apps/' + appId + '/clone', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.restoreApp = function (appId, backupId, callback) { var data = { backupId: backupId }; post('/api/v1/apps/' + appId + '/restore', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.backupApp = function (appId, callback) { var data = {}; post('/api/v1/apps/' + appId + '/backup', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.uninstallApp = function (appId, callback) { var data = {}; post('/api/v1/apps/' + appId + '/uninstall', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.configureApp = function (id, setting, data, callback) { post('/api/v1/apps/' + id + '/configure/' + setting, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200 && status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.repairApp = function (id, data, callback) { post('/api/v1/apps/' + id + '/repair', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200 && status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.updateApp = function (id, manifest, options, callback) { var data = { appStoreId: manifest.id + '@' + manifest.version, skipBackup: !!options.skipBackup }; post('/api/v1/apps/' + id + '/update', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.startApp = function (id, callback) { post('/api/v1/apps/' + id + '/start', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.stopApp = function (id, callback) { post('/api/v1/apps/' + id + '/stop', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.restartApp = function (id, callback) { post('/api/v1/apps/' + id + '/restart', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.debugApp = function (id, state, callback) { var data = { debugMode: state ? { readonlyRootfs: false, cmd: [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ] } : null }; post('/api/v1/apps/' + id + '/configure/debug_mode', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.version = function (callback) { get('/api/v1/cloudron/status', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getStatus = function (callback) { get('/api/v1/cloudron/status', null, function (error, data, status) { if (error) return callback(error); if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setBackupConfig = function (backupConfig, callback) { post('/api/v1/settings/backup_config', backupConfig, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getBackupConfig = function (callback) { get('/api/v1/settings/backup_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getSupportConfig = function (callback) { get('/api/v1/settings/support_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setExternalLdapConfig = function (config, callback) { post('/api/v1/settings/external_ldap_config', config, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getExternalLdapConfig = function (callback) { get('/api/v1/settings/external_ldap_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setDirectoryConfig = function (config, callback) { post('/api/v1/settings/directory_config', config, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getDirectoryConfig = function (callback) { get('/api/v1/settings/directory_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; // network Client.prototype.setSysinfoConfig = function (config, callback) { post('/api/v1/settings/sysinfo_config', config, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getServerIp = function (callback) { get('/api/v1/cloudron/server_ip', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.ip); }); }; Client.prototype.getSysinfoConfig = function (callback) { get('/api/v1/settings/sysinfo_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getBlocklist = function (callback) { var config = {}; get('/api/v1/network/blocklist', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.blocklist); }); }; Client.prototype.setBlocklist = function (blocklist, callback) { post('/api/v1/network/blocklist', { blocklist: blocklist }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setDynamicDnsConfig = function (enabled, callback) { post('/api/v1/settings/dynamic_dns', { enabled: enabled }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getDynamicDnsConfig = function (callback) { get('/api/v1/settings/dynamic_dns', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.enabled); }); }; // branding Client.prototype.setFooter = function (footer, callback) { post('/api/v1/branding/footer', { footer: footer }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getFooter = function (callback) { get('/api/v1/branding/footer', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.footer); }); }; Client.prototype.setUnstableAppsConfig = function (enabled, callback) { post('/api/v1/settings/unstable_apps', { enabled: enabled }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getUnstableAppsConfig = function (callback) { get('/api/v1/settings/unstable_apps', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.enabled); }); }; Client.prototype.setRegistryConfig = function (rc, callback) { post('/api/v1/settings/registry_config', rc, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getRegistryConfig = function (callback) { get('/api/v1/settings/registry_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getUpdateInfo = function (callback) { if (!this._userInfo.isAtLeastAdmin) return callback(new Error('Not allowed')); get('/api/v1/cloudron/update', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.checkForUpdates = function (callback) { post('/api/v1/cloudron/check_for_updates', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); client.refreshConfig(callback); }); }; Client.prototype.setAutoupdatePattern = function (pattern, callback) { post('/api/v1/settings/autoupdate_pattern', { pattern: pattern }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getAutoupdatePattern = function (callback) { get('/api/v1/settings/autoupdate_pattern', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setTimeZone = function (timeZone, callback) { post('/api/v1/settings/time_zone', { timeZone: timeZone }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getTimeZone = function (callback) { get('/api/v1/settings/time_zone', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.timeZone); }); }; Client.prototype.setLanguage = function (language, callback) { post('/api/v1/settings/language', { language: language }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getLanguage = function (callback) { get('/api/v1/settings/language', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.language); }); }; Client.prototype.getRemoteSupport = function (callback) { get('/api/v1/support/remote_support', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.enabled); }); }; Client.prototype.enableRemoteSupport = function (enable, callback) { post('/api/v1/support/remote_support', { enable: enable }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getBackups = function (callback) { get('/api/v1/backups', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.backups); }); }; Client.prototype.getLatestTaskByType = function (type, callback) { get('/api/v1/tasks?page=1&per_page=1&type=' + type, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.tasks.length ? data.tasks[0] : null); }); }; Client.prototype.getTask = function (taskId, callback) { get('/api/v1/tasks/' + taskId, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getTaskLogs = function (taskId, follow, lines, callback) { if (follow) { var eventSource = new EventSource(client.apiOrigin + '/api/v1/tasks/' + taskId + '/logstream?lines=' + lines + '&access_token=' + token); callback(null, eventSource); } else { get('/api/v1/services/' + taskId + '/logs?lines=' + lines, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); } }; Client.prototype.startBackup = function (callback) { post('/api/v1/backups/create', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.cleanupBackups = function (callback) { post('/api/v1/backups/cleanup', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.stopTask = function (taskId, callback) { post('/api/v1/tasks/' + taskId + '/stop', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.restore = function (backupConfig, backupId, version, sysinfoConfig, skipDnsSetup, callback) { var data = { backupConfig: backupConfig, backupId: backupId, version: version, sysinfoConfig: sysinfoConfig, skipDnsSetup: skipDnsSetup }; post('/api/v1/cloudron/restore', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status)); callback(null); }); }; Client.prototype.importBackup = function (appId, backupId, backupFormat, backupConfig, callback) { var data = { backupId: backupId, backupFormat: backupFormat, backupConfig: backupConfig, }; post('/api/v1/apps/' + appId + '/import', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status)); callback(null); }); }; Client.prototype.getNotifications = function (options, page, perPage, callback) { var config = { params: { page: page, per_page: perPage } }; if (typeof options.acknowledged === 'boolean') config.params.acknowledged = options.acknowledged; get('/api/v1/notifications', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.notifications); }); }; Client.prototype.ackNotification = function (notificationId, acknowledged, callback) { post('/api/v1/notifications/' + notificationId, { acknowledged: acknowledged }, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status)); callback(null); }); }; Client.prototype.getEvent = function (eventId, callback) { get('/api/v1/cloudron/eventlog/' + eventId, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.event); }); }; Client.prototype.getEventLogs = function (actions, search, page, perPage, callback) { var config = { params: { actions: actions, search: search, page: page, per_page: perPage } }; get('/api/v1/cloudron/eventlog', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.eventlogs); }); }; Client.prototype.getPlatformLogs = function (unit, follow, lines, callback) { if (follow) { var eventSource = new EventSource(client.apiOrigin + '/api/v1/cloudron/logstream/' + unit + '?lines=' + lines + '&access_token=' + token); callback(null, eventSource); } else { get('/api/v1/cloudron/logs/' + unit + '?lines=' + lines, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); } }; Client.prototype.getServiceLogs = function (serviceName, follow, lines, callback) { if (follow) { var eventSource = new EventSource(client.apiOrigin + '/api/v1/services/' + serviceName + '/logstream?lines=' + lines + '&access_token=' + token); callback(null, eventSource); } else { get('/api/v1/services/' + serviceName + '/logs?lines=' + lines, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); } }; Client.prototype.getApps = function (callback) { var that = this; get('/api/v1/apps', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); var apps = data.apps; for (var i = 0; i < apps.length; i++) { that._appPostProcess(apps[i]); // this will also set the correct iconUrl } callback(null, apps); }); }; Client.prototype.getAppLogs = function (appId, follow, lines, callback) { if (follow) { var eventSource = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream?lines=' + lines + '&access_token=' + token); callback(null, eventSource); } else { get('/api/v1/apps/' + appId + '/logs', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); } }; Client.prototype.getAppBackups = function (appId, callback) { get('/api/v1/apps/' + appId + '/backups', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.backups); }); }; Client.prototype.getServices = function (callback) { get('/api/v1/services', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.services); }); }; Client.prototype.getService = function (serviceName, callback) { get('/api/v1/services/' + serviceName, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.service); }); }; Client.prototype.configureService = function (serviceName, data, callback) { post('/api/v1/services/' + serviceName, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.restartService = function (serviceName, callback) { post('/api/v1/services/' + serviceName + '/restart', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.rebuildService = function (serviceName, callback) { post('/api/v1/services/' + serviceName + '/rebuild', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getUsers = function (search, page, perPage, callback) { if (typeof search === 'function') { callback = search; search = ''; page = 1; perPage = 5000; } var config = { params: { page: page, per_page: perPage } }; if (search) config.params.search = search; get('/api/v1/users', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.users); }); }; Client.prototype.getUser = function (userId, callback) { get('/api/v1/users/' + userId, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getGroups = function (callback) { get('/api/v1/groups', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.groups); }); }; Client.prototype.setGroups = function (userId, groupIds, callback) { put('/api/v1/users/' + userId + '/groups', { groupIds: groupIds }, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getGroup = function (groupId, callback) { get('/api/v1/groups/' + groupId, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.createGroup = function (name, callback) { var data = { name: name }; post('/api/v1/groups', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.updateGroup = function (id, name, callback) { var data = { name: name }; post('/api/v1/groups/' + id, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setGroupMembers = function (id, userIds, callback) { var data = { userIds: userIds }; put('/api/v1/groups/' + id + '/members', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.removeGroup = function (groupId, callback) { var config = { data: {}, headers: { 'Content-Type': 'application/json' } }; del('/api/v1/groups/' + groupId, config, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getApp = function (appId, callback) { var that = this; get('/api/v1/apps/' + appId, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); that._appPostProcess(data); callback(null, data); }); }; Client.prototype.getAppWithTask = function (appId, callback) { var that = this; this.getApp(appId, function (error, app) { if (error) return callback(error); if (!app.taskId) return callback(null, app); that.getTask(app.taskId, function (error, task) { if (error) return callback(error); if (task) { app.progress = task.percent; 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; } callback(null, app); }); }); }; Client.prototype.getCachedAppSync = function (appId) { var appFound = null; this._installedApps.some(function (app) { if (app.id === appId) { appFound = app; return true; } else { return false; } }); return appFound; }; Client.prototype.sendInvite = function (userId, callback) { post('/api/v1/users/' + userId + '/send_invite', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.disableTwoFactorAuthenticationByUserId = function (userId, callback) { post('/api/v1/users/' + userId + '/twofactorauthentication_disable', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setup = function (data, callback) { post('/api/v1/cloudron/setup', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.createAdmin = function (data, callback) { var that = this; post('/api/v1/cloudron/activate', data, null, function (error, result, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, result)); that.setToken(result.token); that.setUserInfo({ username: data.username, email: data.email, admin: true, twoFactorAuthenticationEnabled: false, source: '', avatarUrl: null }); callback(null, result.activated); }); }; Client.prototype.getTokens = function (callback) { get('/api/v1/tokens/', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.tokens); }); }; Client.prototype.createToken = function (name, callback) { var data = { name: name }; post('/api/v1/tokens', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; // FIXME clashes with existing getToken() // Client.prototype.getToken = function (id, callback) { // get('/api/v1/tokens/' + id, null, function (error, data, status) { // if (error) return callback(error); // if (status !== 200) return callback(new ClientError(status, data)); // callback(null, data.token); // }); // }; Client.prototype.delToken = function (tokenId, callback) { del('/api/v1/tokens/' + tokenId, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.addAppPassword = function (identifier, name, callback) { var data = { identifier: identifier, name: name }; post('/api/v1/app_passwords', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getAppPasswords = function (callback) { get('/api/v1/app_passwords', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.delAppPassword = function (id, callback) { del('/api/v1/app_passwords/' + id, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.update = function (options, callback) { var data = { skipBackup: !!options.skipBackup }; post('/api/v1/cloudron/update', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.isRebootRequired = function (callback) { get('/api/v1/cloudron/reboot', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.rebootRequired); }); }; Client.prototype.reboot = function (callback) { post('/api/v1/cloudron/reboot', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setCertificate = function (certificateFile, keyFile, callback) { var data = { cert: certificateFile, key: keyFile }; post('/api/v1/settings/certificate', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.disks = function (callback) { get('/api/v1/cloudron/disks', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.memory = function (callback) { get('/api/v1/cloudron/memory', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.graphs = function (targets, from, options, callback) { // if we have a lot of apps, targets can be very large. node will just disconnect since it exceeds header size var size = 10, chunks = []; for (var i = 0; i < targets.length; i += size) { chunks.push(targets.slice(i, i+size)); } var result = []; async.eachSeries(chunks, function (chunk, iteratorCallback) { var config = { params: { target: chunk, format: 'json', from: from, until: 'now' } }; if (options.noNullPoints) config.params.noNullPoints = true; get('/api/v1/cloudron/graphs', config, function (error, data, status) { if (error) return iteratorCallback(error); if (status !== 200) return iteratorCallback(new ClientError(status, data)); // the datapoint returned here is an [value, timestamp] result = result.concat(data); iteratorCallback(null); }); }, function iteratorDone(error) { callback(error, result); }); }; Client.prototype.createTicket = function (ticket, callback) { // just to be eplicit here var data = { enableSshSupport: !!ticket.enableSshSupport, type: ticket.type, subject: ticket.subject, description: ticket.description, appId: ticket.appId || undefined, altEmail: ticket.altEmail || undefined }; post('/api/v1/support/ticket', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.addUser = function (user, callback) { var data = { email: user.email, displayName: user.displayName, role: user.role }; if (user.username !== null) data.username = user.username; if (user.password !== null) data.password = user.password; post('/api/v1/users', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data.id); }); }; Client.prototype.updateUser = function (user, callback) { var data = { email: user.email, displayName: user.displayName, fallbackEmail: user.fallbackEmail, active: user.active, role: user.role }; post('/api/v1/users/' + user.id, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.changeOwnership = function (userId, callback) { post('/api/v1/users/' + userId + '/make_owner', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.removeUser = function (userId, callback) { var config = { data: {}, headers: { 'Content-Type': 'application/json' } }; del('/api/v1/users/' + userId, config, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.updateProfile = function (data, callback) { post('/api/v1/profile', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.changeAvatar = function (avatarFileOrType, callback) { // Blob type if object if (typeof avatarFileOrType === 'object') { var fd = new FormData(); fd.append('avatar', avatarFileOrType); var config = { headers: { 'Content-Type': undefined }, transformRequest: angular.identity }; post('/api/v1/profile/avatar', fd, config, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); } else { post('/api/v1/profile/avatar', { avatar: avatarFileOrType === 'gravatar' ? 'gravatar' : '' }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); } }; Client.prototype.changePassword = function (currentPassword, newPassword, callback) { var data = { password: currentPassword, newPassword: newPassword }; post('/api/v1/profile/password', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.sendPasswordReset = function (identifier, callback) { post('/api/v1/cloudron/password_reset_request', { identifier }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setTwoFactorAuthenticationSecret = function (callback) { var data = {}; post('/api/v1/profile/twofactorauthentication_secret', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.enableTwoFactorAuthentication = function (totpToken, callback) { var data = { totpToken: totpToken }; post('/api/v1/profile/twofactorauthentication_enable', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.disableTwoFactorAuthentication = function (password, callback) { var data = { password: password }; post('/api/v1/profile/twofactorauthentication_disable', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setGhost = function (userId, password, expiresAt, callback) { var data = { password }; if (expiresAt) data.expiresAt = expiresAt; post('/api/v1/users/' + userId + '/ghost', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.startExternalLdapSync = function (callback) { post('/api/v1/cloudron/sync_external_ldap', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.setUserActive = function (userId, active, callback) { var data = { active: active }; post('/api/v1/users/' + userId + '/active', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.refreshUserInfo = function (callback) { var that = this; callback = typeof callback === 'function' ? callback : function () {}; this.userInfo(function (error, result) { if (error) return callback(error); that.setUserInfo(result); callback(null); }); }; Client.prototype.refreshConfig = function (callback) { var that = this; callback = typeof callback === 'function' ? callback : function () {}; this.config(function (error, result) { if (error) return callback(error); that.getUpdateInfo(function (error, info) { // note: non-admin users may get access denied for this if (!error) result.update = info.update; // attach update information to config object that.setConfig(result); callback(null); }); }); }; Client.prototype.refreshAvailableLanguages = function (callback) { var that = this; get('/api/v1/cloudron/languages', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); angular.copy(data.languages, that._availableLanguages); callback(null, data.languages); }); }; Client.prototype._appPostProcess = function (app) { // calculate the icon paths app.iconUrl = app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token + '&ts=' + app.ts) : null; // amend the post install confirm state app.pendingPostInstallConfirmation = !!localStorage['confirmPostInstall_' + app.id]; if (app.manifest.description) { // can be empty for dev apps var tmp = app.manifest.description.match(/\(.*)\<\/upstream\>/i); app.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : ''; } else { app.upstreamVersion = ''; } if (!app.manifest.title) app.manifest.title = 'Untitled'; if (app.manifest.postInstallMessage) { var text= app.manifest.postInstallMessage; // we chose - because underscore has special meaning in markdown text = text.replace(/\$CLOUDRON-APP-LOCATION/g, app.location); 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, this._config.adminFqdn); text = text.replace(/\$CLOUDRON-API-ORIGIN/g, 'https://' + this._config.adminFqdn); text = text.replace(/\$CLOUDRON-USERNAME/g, this._userInfo.username); text = text.replace(/\$CLOUDRON-APP-ID/g, app.id); // [^] matches even newlines. '?' makes it non-greedy if (app.sso) text = text.replace(/[^]*?<\/nosso>/g, ''); else text = text.replace(/[^]*?<\/sso>/g, ''); app.manifest.postInstallMessage = text; } return app; }; function binarySearch(array, pred) { var lo = -1, hi = array.length; while (1 + lo !== hi) { var mi = lo + ((hi - lo) >> 1); if (pred(array[mi])) { hi = mi; } else { lo = mi; } } return hi; } Client.prototype._updateAppCache = function (app) { var tmp = {}; angular.copy(app, tmp); var foundIndex = this._installedApps.findIndex(function (a) { return a.id === app.id; }); // we replace new data into the existing reference to keep angular bindings if (foundIndex !== -1) { angular.copy(tmp, this._installedApps[foundIndex]); } else { this._installedApps.push(tmp); } // add reference to object map with appId keys this._installedAppsById[app.id] = this._installedApps[foundIndex]; // TODO this not very elegant // update app tags tmp = this._installedApps .map(function (app) { return app.tags || []; }) // return array of arrays .reduce(function (a, i) { return a.concat(i); }, []) // merge all arrays into one .filter(function (v, i, self) { return self.indexOf(v) === i; }) // filter duplicates .sort(function (a, b) { return a.localeCompare(b); }); // sort // keep tag array references angular.copy(tmp, this._appTags); }; Client.prototype.refreshInstalledApps = function (callback) { callback = callback || function (error) { if (error) console.error(error); }; var that = this; this.getApps(function (error, apps) { if (error) return callback(error); async.eachLimit(apps, 20, function (app, iteratorCallback) { app.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['proxyAuth']) && app.sso; // only fetch if we have permissions if (!that._userInfo.isAtLeastAdmin) { app.progress = 0; app.message = ''; app.taskMinutesActive = 0; that._updateAppCache(app); return iteratorCallback(); } var getTaskFunc = app.taskId ? that.getTask.bind(null, app.taskId) : function (next) { return next(); }; getTaskFunc(function (error, task) { if (error) return iteratorCallback(error); if (task) { app.progress = task.percent; 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; } that._updateAppCache(app); iteratorCallback(); }); }, function iteratorDone(error) { if (error) return callback(error); // filter out old apps, going backwards to allow splicing for (var i = that._installedApps.length - 1; i >= 0; --i) { if (!apps.some(function (elem) { return (elem.id === that._installedApps[i].id); })) { var removed = that._installedApps.splice(i, 1); delete that._installedAppsById[removed[0].id]; } } callback(null); }); }); }; Client.prototype.login = function () { this.setToken(null); window.location.href = '/login.html?returnTo=/' + encodeURIComponent(window.location.hash); }; Client.prototype.logout = function () { var token = this.getToken(); this.setToken(null); // invalidates the token window.location.href = client.apiOrigin + '/api/v1/cloudron/logout?access_token=' + token; }; Client.prototype.uploadFile = function (appId, file, progressCallback, callback) { var fd = new FormData(); fd.append('file', file); var config = { headers: { 'Content-Type': undefined }, transformRequest: angular.identity, uploadEventHandlers: { progress: progressCallback } }; post('/api/v1/apps/' + appId + '/upload?file=' + encodeURIComponent('/tmp/' + file.name), fd, config, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.checkDownloadableFile = function (appId, filePath, callback) { var config = { headers: { 'Content-Type': undefined } }; head('/api/v1/apps/' + appId + '/download?file=' + encodeURIComponent(filePath), config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.sendTestMail = function (domain, to, callback) { var data = { to: to }; post('/api/v1/mail/' + domain + '/send_test_mail', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; // Domains Client.prototype.getDomains = function (callback) { get('/api/v1/domains', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.domains); }); }; Client.prototype.getDomain = function (domain, callback) { get('/api/v1/domains/' + domain, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.checkDNSRecords = function (domain, subdomain, callback) { get('/api/v1/domains/' + domain + '/dns_check?subdomain=' + subdomain, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.addDomain = function (domain, zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown, callback) { var data = { domain: domain, provider: provider, config: config, tlsConfig: tlsConfig, wellKnown: wellKnown }; if (zoneName) data.zoneName = zoneName; var that = this; if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; // hack until we fix the domains.js post('/api/v1/domains', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(); }); }; Client.prototype.updateDomain = function (domain, zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown, callback) { var data = { provider: provider, config: config, tlsConfig: tlsConfig, wellKnown: wellKnown }; if (zoneName) data.zoneName = zoneName; var that = this; if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; put('/api/v1/domains/' + domain, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); that.setDnsRecords({ domain: domain, type: 'mail' }, callback); // this is done so that an out-of-sync dkim key can be synced }); }; Client.prototype.renewCerts = function (callback) { post('/api/v1/cloudron/renew_certs', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.removeDomain = function (domain, callback) { var config = { data: { }, headers: { 'Content-Type': 'application/json' } }; del('/api/v1/domains/' + domain, config, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.prepareDashboardDomain = function (domain, callback) { var data = { domain: domain }; post('/api/v1/cloudron/prepare_dashboard_domain', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.setDashboardDomain = function (domain, callback) { var data = { domain: domain }; post('/api/v1/cloudron/set_dashboard_domain', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; // Email Client.prototype.getMailEventLogs = function (search, types, page, perPage, callback) { var config = { params: { page: page, types: types, per_page: perPage, search: search } }; get('/api/v1/mailserver/eventlog', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.eventlogs); }); }; Client.prototype.getMailUsage = function (domain, callback) { var config = { params: { domain: domain } }; get('/api/v1/mailserver/usage', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.usage); }); }; Client.prototype.getMailLocation = function (callback) { var config = {}; get('/api/v1/mailserver/location', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); // { subdomain, domain } }); }; Client.prototype.setMailLocation = function (subdomain, domain, callback) { post('/api/v1/mailserver/location', { subdomain: subdomain, domain: domain }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null, { taskId: data.taskId }); }); }; Client.prototype.getMaxEmailSize = function (callback) { var config = {}; get('/api/v1/mailserver/max_email_size', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.size); }); }; Client.prototype.setMaxEmailSize = function (size, callback) { post('/api/v1/mailserver/max_email_size', { size: size }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getSolrConfig = function (callback) { var config = {}; get('/api/v1/mailserver/solr_config', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setSolrConfig = function (enabled, callback) { post('/api/v1/mailserver/solr_config', { enabled: enabled }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getSpamAcl = function (callback) { var config = {}; get('/api/v1/mailserver/spam_acl', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setSpamAcl = function (acl, callback) { post('/api/v1/mailserver/spam_acl', { whitelist: acl.whitelist, blacklist: acl.blacklist }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getSpamCustomConfig = function (callback) { var config = {}; get('/api/v1/mailserver/spam_custom_config', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.config); }); }; Client.prototype.setSpamCustomConfig = function (config, callback) { post('/api/v1/mailserver/spam_custom_config', { config: config }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getMailConfigForDomain = function (domain, callback) { get('/api/v1/mail/' + domain, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.enableMailForDomain = function (domain, enabled, callback) { post('/api/v1/mail/' + domain + '/enable', { enabled: enabled }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setDnsRecords = function (options, callback) { post('/api/v1/cloudron/sync_dns', options, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data.taskId); }); }; Client.prototype.getMailStatusForDomain = function (domain, callback) { get('/api/v1/mail/' + domain + '/status', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.setMailRelay = function (domain, data, callback) { post('/api/v1/mail/' + domain + '/relay', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setMailBanner = function (domain, data, callback) { post('/api/v1/mail/' + domain + '/banner', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setCatchallAddresses = function (domain, addresses, callback) { post('/api/v1/mail/' + domain + '/catch_all', { addresses: addresses }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.setMailFromValidation = function (domain, enabled, callback) { post('/api/v1/mail/' + domain + '/mail_from_validation', { enabled: enabled }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; // Mailboxes Client.prototype.getMailboxCount = function (domain, callback) { get('/api/v1/mail/' + domain + '/mailbox_count', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.count); }); }; Client.prototype.listMailboxes = function (domain, search, page, perPage, callback) { var config = { params: { search: search, page: page, per_page: perPage } }; get('/api/v1/mail/' + domain + '/mailboxes', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.mailboxes); }); }; Client.prototype.getMailbox = function (domain, name, callback) { get('/api/v1/mail/' + domain + '/mailboxes/' + name, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.mailbox); }); }; Client.prototype.addMailbox = function (domain, name, ownerId, ownerType, callback) { var data = { name: name, ownerId: ownerId, ownerType: ownerType, active: true }; post('/api/v1/mail/' + domain + '/mailboxes', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.updateMailbox = function (domain, name, ownerId, ownerType, active, callback) { var data = { ownerId: ownerId, ownerType: ownerType, active: active }; post('/api/v1/mail/' + domain + '/mailboxes/' + name, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.removeMailbox = function (domain, name, deleteMails, callback) { var config = { data: { deleteMails: deleteMails }, headers: { 'Content-Type': 'application/json' } }; del('/api/v1/mail/' + domain + '/mailboxes/' + name, config, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getAliases = function (name, domain, callback) { var config = { params: { page: 1, per_page: 1000 } }; get('/api/v1/mail/' + domain + '/mailboxes/' + name + '/aliases', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.aliases); }); }; Client.prototype.setAliases = function (name, domain, aliases, callback) { var data = { aliases: aliases }; put('/api/v1/mail/' + domain + '/mailboxes/' + name + '/aliases', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.listMailingLists = function (domain, search, page, perPage, callback) { var config = { params: { search: search, page: page, per_page: perPage } }; get('/api/v1/mail/' + domain + '/lists', config, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.lists); }); }; Client.prototype.getMailingList = function (domain, name, callback) { get('/api/v1/mail/' + domain + '/lists/' + name, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.list); }); }; Client.prototype.addMailingList = function (domain, name, members, membersOnly, callback) { var data = { name: name, members: members, membersOnly: membersOnly, active: true }; post('/api/v1/mail/' + domain + '/lists', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.updateMailingList = function (domain, name, members, membersOnly, active, callback) { var data = { members: members, membersOnly: membersOnly, active: active }; post('/api/v1/mail/' + domain + '/lists/' + name, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.removeMailingList = function (domain, name, callback) { del('/api/v1/mail/' + domain + '/lists/' + name, null, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; // Volumes Client.prototype.getVolumes = function (callback) { get('/api/v1/volumes', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data.volumes); }); }; Client.prototype.getVolume = function (volume, callback) { get('/api/v1/volumes/' + volume, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getVolumeStatus = function (volume, callback) { get('/api/v1/volumes/' + volume + '/status', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.addVolume = function (name, mountType, hostPath, mountOptions, callback) { var data = { name: name, mountType: mountType, mountOptions: mountOptions }; if (hostPath) data.hostPath = hostPath; var that = this; post('/api/v1/volumes', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data.id); }); }; Client.prototype.updateVolume = function (volumeId, mountType, mountOptions, callback) { var data = { mountType: mountType, mountOptions: mountOptions }; var that = this; post('/api/v1/volumes/' + volumeId, data, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(); }); }; Client.prototype.removeVolume = function (volume, callback) { var config = { data: { }, headers: { 'Content-Type': 'application/json' } }; del('/api/v1/volumes/' + volume, config, function (error, data, status) { if (error) return callback(error); if (status !== 204) return callback(new ClientError(status, data)); callback(null); }); }; Client.prototype.getAppstoreUserToken = function (callback) { post('/api/v1/appstore/user_token', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null, data.accessToken); }); }; // This will change the location Client.prototype.openSubscriptionSetup = function (subscription) { // we only allow the owner to do so if (!this._userInfo.isAtLeastOwner) return; // basically the user has not setup appstore account yet if (!subscription.plan) return window.location.href = '/#/appstore'; var that = this; var email = subscription.emailEncoded; var cloudronId = subscription.cloudronId; if (!this._userInfo.isAtLeastOwner) return window.location.href = that.getConfig().webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + email + '&cloudronId=' + cloudronId; this.getAppstoreUserToken(function (error, token) { if (error) console.error('Unable to get appstore user token.', error); var url = that.getConfig().webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + email + '&token=' + token; // Only open the subscription setup dialog when no subscription exists if (!subscription.plan || subscription.plan.id === 'free') url += '&cloudronId=' + cloudronId window.location.href = url; }); }; Client.prototype.getAppstoreAppByIdAndVersion = function (appId, version, callback) { var url = '/api/v1/appstore/apps/' + appId; if (version && version !== 'latest') url += '/versions/' + version; get(url, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.getAppstoreApps = function (callback) { var that = this; get('/api/v1/appstore/apps', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); angular.copy(data.apps, that._appstoreAppCache); return callback(null, that._appstoreAppCache); }); }; Client.prototype.getAppstoreAppsFast = function (callback) { if (this._appstoreAppCache.length !== 0) return callback(null, this._appstoreAppCache); this.getAppstoreApps(callback); }; Client.prototype.getSubscription = function (callback) { if (!this._userInfo.isAtLeastAdmin) return callback(new Error('Not allowed')); get('/api/v1/appstore/subscription', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); // just some helper property, since angular bindings cannot dot his easily data.emailEncoded = encodeURIComponent(data.email); callback(null, data); // { email, plan: { id, name }, cancel_at, status } }); }; Client.prototype.registerCloudron = function (email, password, totpToken, signup, callback) { var data = { email: email, password: password, signup: signup, }; if (totpToken) data.totpToken = totpToken; post('/api/v1/appstore/register_cloudron', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 201) return callback(new ClientError(status, data)); callback(null); }); }; // FileManager API // mode can be 'download', 'open', 'link' or 'data' Client.prototype.filesGet = function (id, type, path, mode, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; if (mode === 'download') { window.open(client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=true&access_token=' + token); callback(null); } else if (mode === 'open') { window.open(client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=false&access_token=' + token); callback(null); } else if (mode === 'link') { callback(null, client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=false&access_token=' + token); } else { function responseHandler(data, headers, status) { if (headers()['content-type'] && headers()['content-type'].indexOf('application/json') !== -1) return JSON.parse(data); return data; } get('/api/v1/' + objpath + '/files/' + path, { transformResponse: responseHandler }, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); } }; Client.prototype.filesRemove = function (id, type, path, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; del('/api/v1/' + objpath + '/files/' + path, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesExtract = function (id, type, path, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; put('/api/v1/' + objpath + '/files/' + path, { action: 'extract' }, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesChown = function (id, type, path, uid, recursive, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; put('/api/v1/' + objpath + '/files/' + path, { action: 'chown', uid: uid, recursive: recursive }, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesRename = function (id, type, path, newPath, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; put('/api/v1/' + objpath + '/files/' + path, { action: 'rename', newFilePath: decodeURIComponent(newPath) }, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesCopy = function (id, type, path, newPath, callback) { var that = this; var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; put('/api/v1/' + objpath + '/files/' + path, { action: 'copy', newFilePath: decodeURIComponent(newPath) }, {}, function (error, data, status) { if (error && error.statusCode === 409) return that.filesCopy(id, type, path, newPath + '-copy', callback); if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesCreateDirectory = function (id, type, path, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; post('/api/v1/' + objpath + '/files/' + path, { directory: decodeURIComponent(path) }, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesCreateFile = function (id, type, path, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; post('/api/v1/' + objpath + '/files/' + path, {}, {}, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); }); }; Client.prototype.filesUpload = function (id, type, path, file, overwrite, progressHandler, callback) { var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id; var fd = new FormData(); fd.append('file', file); if (overwrite) fd.append('overwrite', 'true'); function done(error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); callback(null, data); } $http({ url: client.apiOrigin + '/api/v1/' + objpath + '/files/' + path, method: 'POST', data: fd, transformRequest: angular.identity, headers: { 'Content-Type': undefined, Authorization: 'Bearer ' + token }, uploadEventHandlers: { progress: function (e) { progressHandler(e.loaded); } } }).success(defaultSuccessHandler(done)).error(defaultErrorHandler(done)); }; // ---------------------------------------------- // Eventlog helpers // ---------------------------------------------- Client.prototype.eventLogDetails = function (eventLog) { var ACTION_ACTIVATE = 'cloudron.activate'; var ACTION_PROVISION = 'cloudron.provision'; var ACTION_RESTORE = 'cloudron.restore'; var ACTION_APP_CLONE = 'app.clone'; var ACTION_APP_REPAIR = 'app.repair'; var ACTION_APP_CONFIGURE = 'app.configure'; var ACTION_APP_INSTALL = 'app.install'; var ACTION_APP_RESTORE = 'app.restore'; var ACTION_APP_IMPORT = 'app.import'; var ACTION_APP_UNINSTALL = 'app.uninstall'; var ACTION_APP_UPDATE = 'app.update'; var ACTION_APP_UPDATE_FINISH = 'app.update.finish'; var ACTION_APP_LOGIN = 'app.login'; var ACTION_APP_OOM = 'app.oom'; var ACTION_APP_UP = 'app.up'; var ACTION_APP_DOWN = 'app.down'; var ACTION_APP_START = 'app.start'; var ACTION_APP_STOP = 'app.stop'; var ACTION_APP_RESTART = 'app.restart'; var ACTION_BACKUP_FINISH = 'backup.finish'; var ACTION_BACKUP_START = 'backup.start'; var ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start'; var ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish'; var ACTION_CERTIFICATE_NEW = 'certificate.new'; var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew'; var ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update'; var ACTION_DOMAIN_ADD = 'domain.add'; var ACTION_DOMAIN_UPDATE = 'domain.update'; var ACTION_DOMAIN_REMOVE = 'domain.remove'; var ACTION_START = 'cloudron.start'; var ACTION_UPDATE = 'cloudron.update'; var ACTION_UPDATE_FINISH = 'cloudron.update.finish'; var ACTION_USER_ADD = 'user.add'; var ACTION_USER_LOGIN = 'user.login'; var ACTION_USER_LOGOUT = 'user.logout'; var ACTION_USER_REMOVE = 'user.remove'; var ACTION_USER_UPDATE = 'user.update'; var ACTION_USER_TRANSFER = 'user.transfer'; var ACTION_MAIL_LOCATION = 'mail.location'; var ACTION_MAIL_ENABLED = 'mail.enabled'; var ACTION_MAIL_DISABLED = 'mail.disabled'; var ACTION_MAIL_MAILBOX_ADD = 'mail.box.add'; var ACTION_MAIL_MAILBOX_UPDATE = 'mail.box.update'; var ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove'; var ACTION_MAIL_LIST_ADD = 'mail.list.add'; var ACTION_MAIL_LIST_UPDATE = 'mail.list.update'; var ACTION_MAIL_LIST_REMOVE = 'mail.list.remove'; var ACTION_SUPPORT_TICKET = 'support.ticket'; var ACTION_SUPPORT_SSH = 'support.ssh'; var ACTION_VOLUME_ADD = 'volume.add'; var ACTION_VOLUME_UPDATE = 'volume.update'; var ACTION_VOLUME_REMOVE = 'volume.remove'; var ACTION_DYNDNS_UPDATE = 'dyndns.update'; var ACTION_SYSTEM_CRASH = 'system.crash'; var data = eventLog.data; var errorMessage = data.errorMessage; var details, app; function appName(app) { return (app.label || app.fqdn || app.location) + ' (' + app.manifest.title + ')'; } switch (eventLog.action) { case ACTION_ACTIVATE: return 'Cloudron was activated'; case ACTION_PROVISION: return 'Cloudron was setup'; case ACTION_RESTORE: return 'Cloudron was restored using backup ' + data.backupId; case ACTION_APP_CONFIGURE: { if (!data.app) return ''; app = data.app; var q = function (x) { return '"' + x + '"'; }; if ('accessRestriction' in data) { // since it can be null return 'Access restriction of ' + appName(app) + ' was changed'; } else if (data.label) { return 'Label of ' + appName(app) + ' was set to ' + q(data.label); } else if (data.tags) { return 'Tags of ' + appName(app) + ' was set to ' + q(data.tags.join(',')); } else if (data.icon) { return 'Icon of ' + appName(app) + ' was changed'; } else if (data.memoryLimit) { return 'Memory limit of ' + appName(app) + ' was set to ' + data.memoryLimit; } else if (data.cpuShares) { return 'CPU shares of ' + appName(app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%'; } else if (data.env) { return 'Env vars of ' + appName(app) + ' was changed'; } else if ('debugMode' in data) { // since it can be null if (data.debugMode) { return appName(app) + ' was placed in repair mode'; } else { return appName(app) + ' was taken out of repair mode'; } } else if ('enableBackup' in data) { return 'Automatic backups of ' + appName(app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled'); } else if ('enableAutomaticUpdate' in data) { return 'Automatic updates of ' + appName(app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled'); } else if ('reverseProxyConfig' in data) { return 'Reverse proxy configuration of ' + appName(app) + ' was updated'; } else if ('cert' in data) { if (data.cert) { return 'Custom certificate was set for ' + appName(app); } else { return 'Certificate of ' + appName(app) + ' was reset'; } } else if (data.location) { if (data.fqdn !== data.app.fqdn) { return 'Location of ' + appName(app) + ' was changed to ' + data.fqdn; } else if (!angular.equals(data.alternateDomains, data.app.alternateDomains)) { var altFqdns = data.alternateDomains.map(function (a) { return a.fqdn; }); return 'Alternate domains of ' + appName(app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset'); } else if (!angular.equals(data.aliasDomains, data.app.aliasDomains)) { var aliasDomains = data.aliasDomains.map(function (a) { return a.fqdn; }); return 'Alias domains of ' + appName(app) + ' was ' + (aliasDomains.length ? 'set to ' + aliasDomains.join(', ') : 'reset'); } else if (!angular.equals(data.portBindings, data.app.portBindings)) { return 'Port bindings of ' + appName(app) + ' was changed'; } } else if ('dataDir' in data) { if (data.dataDir) { return 'Data directory of ' + appName(app) + ' was set ' + data.dataDir; } else { return 'Data directory of ' + appName(app) + ' was reset'; } } else if ('icon' in data) { if (data.icon) { return 'Icon of ' + appName(app) + ' was set'; } else { return 'Icon of ' + appName(app) + ' was reset'; } } else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) { if (data.mailboxName) { return 'Mailbox of ' + appName(app) + ' was set to ' + q(data.mailboxName); } else { return 'Mailbox of ' + appName(app) + ' was reset'; } } return appName(app) + ' was re-configured'; } case ACTION_APP_INSTALL: if (!data.app) return ''; return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed at ' + (data.app.fqdn || data.app.location); case ACTION_APP_RESTORE: if (!data.app) return ''; details = data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location); // older versions (<3.5) did not have these fields if (data.fromManifest) details += ' from version ' + data.fromManifest.version; if (data.toManifest) details += ' to version ' + data.toManifest.version; if (data.backupId) details += ' using backup ' + data.backupId; return details; case ACTION_APP_IMPORT: if (!data.app) return ''; details = data.app.manifest.title + ' was imported at ' + (data.app.fqdn || data.app.location); if (data.toManifest) details += ' to version ' + data.toManifest.version; if (data.backupId) details += ' using backup ' + data.backupId; return details; case ACTION_APP_UNINSTALL: if (!data.app) return ''; return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was uninstalled at ' + (data.app.fqdn || data.app.location); case ACTION_APP_UPDATE: if (!data.app) return ''; return 'Update of ' + data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version; case ACTION_APP_UPDATE_FINISH: if (!data.app) return ''; return data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' was updated to v' + data.app.manifest.version; case ACTION_APP_CLONE: return data.newApp.manifest.title + ' at ' + (data.newApp.fqdn || data.newApp.location) + ' was cloned from ' + (data.oldApp.fqdn || data.oldApp.location) + ' using backup ' + data.backupId + ' with v' + data.oldApp.manifest.version; case ACTION_APP_REPAIR: return 'App ' + appName(data.app) + ' was re-configured'; // re-configure of email apps is more common? case ACTION_APP_LOGIN: { app = this.getCachedAppSync(data.appId); if (!app) return ''; return 'App ' + app.fqdn + ' logged in'; } case ACTION_APP_OOM: if (!data.app) return ''; return appName(data.app) + ' ran out of memory'; case ACTION_APP_DOWN: if (!data.app) return ''; return appName(data.app) + ' is down'; case ACTION_APP_UP: if (!data.app) return ''; return appName(data.app) + ' is back online'; case ACTION_APP_START: if (!data.app) return ''; return appName(data.app) + ' was started'; case ACTION_APP_STOP: if (!data.app) return ''; return appName(data.app) + ' was stopped'; case ACTION_APP_RESTART: if (!data.app) return ''; return appName(data.app) + ' was restarted'; case ACTION_BACKUP_START: return 'Backup started'; case ACTION_BACKUP_FINISH: if (!errorMessage) { return 'Cloudron backup created with Id ' + data.backupId; } else { return 'Cloudron backup errored with error: ' + errorMessage; } case ACTION_BACKUP_CLEANUP_START: return 'Backup cleaner started'; case ACTION_BACKUP_CLEANUP_FINISH: return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + (data.removedBoxBackups ? data.removedBoxBackups.length : '0') + ' backups'; case ACTION_CERTIFICATE_NEW: return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); case ACTION_DASHBOARD_DOMAIN_UPDATE: return 'Dashboard domain set to ' + data.fqdn; case ACTION_DOMAIN_ADD: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added'; case ACTION_DOMAIN_UPDATE: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated'; case ACTION_DOMAIN_REMOVE: return 'Domain ' + data.domain + ' was removed'; case ACTION_MAIL_LOCATION: return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain; case ACTION_MAIL_ENABLED: return 'Mail was enabled for domain ' + data.domain; case ACTION_MAIL_DISABLED: return 'Mail was disabled for domain ' + data.domain; case ACTION_MAIL_MAILBOX_ADD: return 'Mailbox with name ' + data.name + ' was added in domain ' + data.domain; case ACTION_MAIL_MAILBOX_UPDATE: return 'Mailbox with name ' + data.name + ' was updated in domain ' + data.domain; case ACTION_MAIL_MAILBOX_REMOVE: return 'Mailbox with name ' + data.name + ' was removed in domain ' + data.domain; case ACTION_MAIL_LIST_ADD: return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain; case ACTION_MAIL_LIST_UPDATE: return 'Mail list with name ' + data.name + ' was updated in domain ' + data.domain; case ACTION_MAIL_LIST_REMOVE: return 'Mail list with name ' + data.name + ' was removed in domain ' + data.domain; case ACTION_START: return 'Cloudron started with version ' + data.version; case ACTION_UPDATE: return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started'; case ACTION_UPDATE_FINISH: if (data.errorMessage) { return 'Cloudron update errored. Error: ' + data.errorMessage; } else { return 'Cloudron updated to version ' + data.newVersion; } case ACTION_USER_ADD: return data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added'; case ACTION_USER_UPDATE: return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated'; case ACTION_USER_REMOVE: return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed'; case ACTION_USER_TRANSFER: return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId; case ACTION_USER_LOGIN: return (data.user ? data.user.username : data.userId) + ' logged in'; case ACTION_USER_LOGOUT: return (data.user ? data.user.username : data.userId) + ' logged out'; case ACTION_DYNDNS_UPDATE: return 'DNS was updated from ' + data.fromIp + ' to ' + data.toIp; case ACTION_SUPPORT_SSH: return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled'); case ACTION_SUPPORT_TICKET: return 'Support ticket was created'; case ACTION_SYSTEM_CRASH: return 'A system process crashed'; case ACTION_VOLUME_ADD: return 'Volume "' + data.volume.name + '" was added'; case ACTION_VOLUME_UPDATE: return 'Volme "' + data.volume.name + '" was updated'; case ACTION_VOLUME_REMOVE: return 'Volume "' + data.volume.name + '" was removed'; default: return eventLog.action; } } Client.prototype.eventLogSource = function (eventLog) { var source = eventLog.source; var line = ''; line = source.username || source.userId || source.mailboxId || source.authType || 'system'; if (source.appId) { var app = this.getCachedAppSync(source.appId); line += ' - ' + (app ? app.fqdn : source.appId); } else if (source.ip) { line += ' - ' + source.ip; } return line; } client = new Client(); return client; }]);