1085 lines
38 KiB
JavaScript
1085 lines
38 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular */
|
|
/* global $ */
|
|
/* global asyncSeries */
|
|
/* global asyncForEach */
|
|
/* global RSTATES */
|
|
/* global ISTATES */
|
|
/* global ERROR */
|
|
|
|
angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) {
|
|
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
|
|
|
// Avoid full reload on path change
|
|
// https://stackoverflow.com/a/22614334
|
|
// reloadOnUrl: false in $routeProvider did not work!
|
|
var lastRoute = $route.current;
|
|
$scope.$on('$locationChangeSuccess', function (/* event */) {
|
|
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
|
|
$route.current = lastRoute;
|
|
}
|
|
});
|
|
|
|
var appId = $routeParams.appId;
|
|
if (!appId) return $location.path('/apps');
|
|
|
|
$scope.view = '';
|
|
$scope.app = null;
|
|
$scope.config = Client.getConfig();
|
|
$scope.user = Client.getUserInfo();
|
|
$scope.domains = [];
|
|
$scope.groups = [];
|
|
$scope.users = [];
|
|
|
|
$scope.HOST_PORT_MIN = 1024;
|
|
$scope.HOST_PORT_MAX = 65535;
|
|
$scope.ROBOTS_DISABLE_INDEXING_TEMPLATE = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';
|
|
|
|
$scope.setView = function (view) {
|
|
if ($scope.view === view) return;
|
|
|
|
// on error only allow uninstall or debug view
|
|
if ($scope.app.error && view !== 'uninstall') view = 'debug';
|
|
|
|
$route.updateParams({ view: view });
|
|
$scope[view].show();
|
|
$scope.view = view;
|
|
};
|
|
|
|
$scope.postInstallMessage = {
|
|
confirmed: false,
|
|
openApp: false,
|
|
|
|
show: function (openApp) {
|
|
$scope.postInstallMessage.confirmed = false;
|
|
$scope.postInstallMessage.openApp = !!openApp;
|
|
|
|
if (!$scope.app.manifest.postInstallMessage) return;
|
|
$('#postInstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
if (!$scope.postInstallMessage.confirmed) return;
|
|
|
|
$scope.app.pendingPostInstallConfirmation = false;
|
|
delete localStorage['confirmPostInstall_' + $scope.app.id];
|
|
|
|
$('#postInstallModal').modal('hide');
|
|
}
|
|
};
|
|
|
|
$scope.display = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
tags: '',
|
|
label: '',
|
|
icon: { data: null },
|
|
|
|
iconUrl: function () {
|
|
if (!$scope.app) return '';
|
|
|
|
if ($scope.display.icon.data === '__original__') { // user clicked reset
|
|
return $scope.app.iconUrl + '&original=true';
|
|
} else if ($scope.display.icon.data) { // user uploaded icon
|
|
return $scope.display.icon.data;
|
|
} else { // current icon
|
|
return $scope.app.iconUrl;
|
|
}
|
|
},
|
|
|
|
resetCustomIcon: function () {
|
|
$scope.display.icon.data = '__original__';
|
|
},
|
|
|
|
showCustomIconSelector: function () {
|
|
$('#iconFileInput').click();
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.display.error = {};
|
|
|
|
// translate for tag-input
|
|
$scope.display.tags = app.tags ? app.tags.join(',') : '';
|
|
|
|
$scope.display.label = $scope.app.label || '';
|
|
$scope.display.icon = { data: null };
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.display.busy = true;
|
|
$scope.display.error = {};
|
|
|
|
function done(error) {
|
|
if (error) Client.error(error);
|
|
|
|
refreshApp($scope.display.show);
|
|
|
|
$timeout(function () {
|
|
$scope.display.busy = false;
|
|
$scope.display.success = true;
|
|
$scope.displayForm.$setPristine();
|
|
}, 1000);
|
|
}
|
|
|
|
var NOOP = function (next) { return next(); };
|
|
var configureLabel = $scope.display.label === $scope.app.label ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'label', { label: $scope.display.label });
|
|
|
|
configureLabel(function (error) {
|
|
if (error) return done(error);
|
|
|
|
var tags = $scope.display.tags.split(',').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
|
|
|
|
var configureTags = angular.equals(tags, $scope.app.tags) ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'tags', { tags: tags });
|
|
|
|
configureTags(function (error) {
|
|
if (error) return done(error);
|
|
|
|
// skip if icon is unchanged
|
|
if ($scope.display.icon.data === null) return done();
|
|
|
|
var icon;
|
|
if ($scope.display.icon.data === '__original__') { // user reset the icon
|
|
icon = '';
|
|
} else if ($scope.display.icon.data) { // user loaded custom icon
|
|
icon = $scope.display.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'icon', { icon: icon }, function (error) {
|
|
if (error) return done(error);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.location = {
|
|
busy: false,
|
|
error: {},
|
|
domainCollisions: [],
|
|
|
|
domain: null,
|
|
location: '',
|
|
alternateDomains: [],
|
|
portBindings: {},
|
|
portBindingsEnabled: {},
|
|
portBindingsInfo: {},
|
|
|
|
addAlternateDomain: function (event) {
|
|
event.preventDefault();
|
|
$scope.location.alternateDomains.push({
|
|
domain: $scope.domains[0],
|
|
subdomain: ''
|
|
});
|
|
},
|
|
|
|
delAlternateDomain: function (event, index) {
|
|
event.preventDefault();
|
|
$scope.location.alternateDomains.splice(index, 1);
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.location.error = {};
|
|
$scope.location.domainCollisions = [];
|
|
$scope.location.location = app.location;
|
|
$scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
|
|
$scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
|
$scope.location.alternateDomains = app.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
|
|
|
|
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
|
for (var env in $scope.location.portBindingsInfo) {
|
|
if (app.portBindings && app.portBindings[env]) {
|
|
$scope.location.portBindings[env] = app.portBindings[env];
|
|
$scope.location.portBindingsEnabled[env] = true;
|
|
} else {
|
|
$scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0;
|
|
$scope.location.portBindingsEnabled[env] = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
submit: function (overwriteDns) {
|
|
$('#domainCollisionsModal').modal('hide');
|
|
|
|
$scope.location.busy = true;
|
|
$scope.location.error = {};
|
|
$scope.location.domainCollisions = [];
|
|
|
|
// only use enabled ports from portBindings
|
|
var portBindings = {};
|
|
for (var env in $scope.location.portBindings) {
|
|
if ($scope.location.portBindingsEnabled[env]) {
|
|
portBindings[env] = $scope.location.portBindings[env];
|
|
}
|
|
}
|
|
|
|
var data = {
|
|
overwriteDns: !!overwriteDns,
|
|
location: $scope.location.location,
|
|
domain: $scope.location.domain.domain,
|
|
portBindings: portBindings,
|
|
alternateDomains: $scope.location.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
|
|
};
|
|
|
|
// pre-flight only for changed domains
|
|
var domains = [];
|
|
if ($scope.app.domain !== data.domain || $scope.app.location !== data.location) domains.push({ subdomain: data.location, domain: data.domain });
|
|
data.alternateDomains.forEach(function (a) {
|
|
if ($scope.app.alternateDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
|
|
domains.push({ subdomain: a.subdomain, domain: a.domain });
|
|
});
|
|
|
|
asyncForEach(domains, function (domain, callback) {
|
|
if (overwriteDns) return callback();
|
|
|
|
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
|
if (error) return callback(error);
|
|
if (result.error) {
|
|
if (data.domain === domain.domain && data.location === domain.subdomain) {
|
|
$scope.location.error.location = domain.domain + ' ' + result.error.message;
|
|
} else {
|
|
$scope.location.error.alternateDomains = domain.domain + ' ' + result.error.message;
|
|
}
|
|
$scope.location.busy = false;
|
|
return;
|
|
}
|
|
|
|
if (result.needsOverwrite) $scope.location.domainCollisions.push(domain);
|
|
|
|
callback();
|
|
});
|
|
}, function (error) {
|
|
if (error) {
|
|
$scope.location.busy = false;
|
|
return Client.error(error);
|
|
}
|
|
|
|
if ($scope.location.domainCollisions.length) {
|
|
$scope.location.busy = false;
|
|
return $('#domainCollisionsModal').modal('show');
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'location', data, function (error) {
|
|
if (error && (error.statusCode === 409 || error.statusCode === 400)) {
|
|
if ((error.subdomain && error.domain) || error.field === 'location') {
|
|
if (data.domain === error.domain && data.location === error.subdomain) { // the primary
|
|
$scope.location.error.location = error.message;
|
|
$scope.locationForm.$setPristine();
|
|
} else {
|
|
$scope.location.error.alternateDomains = error.message;
|
|
}
|
|
} else if (error.portName || error.field === 'portBindings') {
|
|
$scope.location.error.port = error.message;
|
|
}
|
|
|
|
$scope.location.busy = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.locationForm.$setPristine();
|
|
$scope.location.busy = false;
|
|
|
|
refreshApp();
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.access = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
ftp: false,
|
|
ssoAuth: false,
|
|
accessRestrictionOption: 'any',
|
|
accessRestriction: { users: [], groups: [] },
|
|
|
|
isAccessRestrictionValid: function () {
|
|
var tmp = $scope.access.accessRestriction;
|
|
return !!(tmp.users.length || tmp.groups.length);
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.access.error = {};
|
|
$scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
|
$scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
|
|
$scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
|
$scope.access.accessRestriction = { users: [], groups: [] };
|
|
|
|
if (app.accessRestriction) {
|
|
var userSet = { };
|
|
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
|
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); });
|
|
|
|
var groupSet = { };
|
|
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
|
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); });
|
|
}
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.access.busy = true;
|
|
$scope.access.error = {};
|
|
|
|
var accessRestriction = null;
|
|
if ($scope.access.accessRestrictionOption === 'groups') {
|
|
accessRestriction = { users: [], groups: [] };
|
|
accessRestriction.users = $scope.access.accessRestriction.users.map(function (u) { return u.id; });
|
|
accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; });
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.access.success = true;
|
|
$scope.access.busy = false;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.resources = {
|
|
busy: false,
|
|
busyDataDir: false,
|
|
error: {},
|
|
|
|
currentMemoryLimit: 0,
|
|
memoryLimit: 0,
|
|
memoryTicks: [],
|
|
dataDir: null,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.resources.error = {};
|
|
$scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
|
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
|
|
$scope.resources.dataDir = app.dataDir;
|
|
|
|
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
|
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
|
$scope.resources.memoryTicks = [];
|
|
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
|
|
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
|
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024);
|
|
}
|
|
if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) {
|
|
$scope.resources.memoryTicks.unshift(app.manifest.memoryLimit);
|
|
}
|
|
},
|
|
|
|
submitMemoryLimit: function () {
|
|
$scope.resources.busy = true;
|
|
$scope.resources.error = {};
|
|
|
|
var memoryLimit = $scope.resources.memoryLimit === $scope.resources.memoryTicks[0] ? 0 : $scope.resources.memoryLimit;
|
|
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.resources.currentMemoryLimit = $scope.resources.memoryLimit;
|
|
$scope.resources.busy = false;
|
|
|
|
refreshApp();
|
|
});
|
|
},
|
|
|
|
submitDataDir: function () {
|
|
$scope.resources.busyDataDir = true;
|
|
$scope.resources.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'data_dir', { dataDir: $scope.resources.dataDir || null }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.resources.error.dataDir = error.message;
|
|
$scope.resources.busyDataDir = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.resourcesDataDirForm.$setPristine();
|
|
$scope.resources.busyDataDir = false;
|
|
|
|
refreshApp();
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.email = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
mailboxName: '',
|
|
mailboxDomain: '',
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.emailForm.$setPristine();
|
|
$scope.email.error = {};
|
|
$scope.email.mailboxName = app.mailboxName || '';
|
|
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === app.mailboxDomain; })[0];
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.email.error = {};
|
|
$scope.email.busy = true;
|
|
|
|
Client.configureApp($scope.app.id, 'mailbox', { mailboxName: $scope.email.mailboxName || null, mailboxDomain: $scope.email.mailboxDomain.domain }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.email.busy = false;
|
|
$scope.email.error.mailboxName = error.message;
|
|
$scope.emailForm.$setPristine();
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.emailForm.$setPristine();
|
|
|
|
$scope.email.busy = false;
|
|
|
|
refreshApp(function (error) {
|
|
if (error) return;
|
|
// when the mailboxName is 'reset', this will fill it up with the default again
|
|
$scope.email.mailboxName = $scope.app.mailboxName || '';
|
|
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === $scope.app.mailboxDomain; })[0];
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.security = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
robotsTxt: '',
|
|
csp: '',
|
|
|
|
show: function () {
|
|
$scope.security.error = {};
|
|
$scope.security.robotsTxt = $scope.app.reverseProxyConfig.robotsTxt || '';
|
|
$scope.security.csp = $scope.app.reverseProxyConfig.csp || '';
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.security.busy = true;
|
|
$scope.security.error = {};
|
|
|
|
var reverseProxyConfig = {
|
|
robotsTxt: $scope.security.robotsTxt || null, // empty string resets
|
|
csp: $scope.security.csp || null // empty string resets
|
|
};
|
|
|
|
Client.configureApp($scope.app.id, 'reverse_proxy', reverseProxyConfig, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.security.success = true;
|
|
$scope.security.busy = false;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.updates = {
|
|
busy: false,
|
|
busyCheck: false,
|
|
busyUpdate: false,
|
|
skipBackup: false,
|
|
|
|
enableAutomaticUpdate: false,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate;
|
|
},
|
|
|
|
toggleAutomaticUpdates: function () {
|
|
$scope.updates.busy = true;
|
|
|
|
Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.updates.enableAutomaticUpdate }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.updates.enableAutomaticUpdate = !$scope.updates.enableAutomaticUpdate;
|
|
$scope.updates.busy = false;
|
|
}, 1000);
|
|
});
|
|
},
|
|
|
|
check: function () {
|
|
$scope.updates.busyCheck = true;
|
|
|
|
Client.checkForUpdates(function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.updates.busyCheck = false;
|
|
});
|
|
},
|
|
|
|
askUpdate: function () {
|
|
$scope.updates.busyUpdate = false;
|
|
$('#updateModal').modal('show');
|
|
},
|
|
|
|
confirmUpdate: function () {
|
|
$scope.updates.busyUpdate = true;
|
|
|
|
Client.updateApp($scope.app.id, $scope.config.update.apps[$scope.app.id].manifest, { skipBackup: $scope.updates.skipBackup }, function (error) {
|
|
$scope.updates.busyUpdate = false;
|
|
if (error) return Client.error(error);
|
|
|
|
$('#updateModal').modal('hide');
|
|
refreshApp();
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.backups = {
|
|
busy: false,
|
|
busyCreate: false,
|
|
error: {},
|
|
copyBackupIdDone: false,
|
|
|
|
enableBackup: false,
|
|
backups: [],
|
|
|
|
copyBackupId: function (backup) {
|
|
var copyText = document.getElementById('backupIdHelper');
|
|
copyText.value = backup.id;
|
|
copyText.select();
|
|
document.execCommand('copy');
|
|
|
|
$scope.backups.copyBackupIdDone = true;
|
|
|
|
// reset after 2.5sec
|
|
$timeout(function () { $scope.backups.copyBackupIdDone = false; }, 2500);
|
|
},
|
|
|
|
trackBackupTask: function () {
|
|
$scope.backups.busyCreate = true;
|
|
|
|
refreshApp(function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.backups.busyCreate = false;
|
|
|
|
waitForAppTask(function (error) {
|
|
if (error) return Client.error(error);
|
|
$scope.backups.show();
|
|
});
|
|
});
|
|
},
|
|
|
|
createBackup: function () {
|
|
$scope.backups.busyCreate = true;
|
|
|
|
Client.backupApp($scope.app.id, function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.backups.trackBackupTask();
|
|
});
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.backups.error = {};
|
|
$scope.backups.enableBackup = app.enableBackup;
|
|
|
|
Client.getAppBackups(app.id, function (error, backups) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.backups.backups = backups;
|
|
});
|
|
},
|
|
|
|
toggleAutomaticBackups: function () {
|
|
$scope.backups.busy = true;
|
|
$scope.backups.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'automatic_backup', { enable: !$scope.backups.enableBackup }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.backups.enableBackup = !$scope.backups.enableBackup;
|
|
$scope.backups.busy = false;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.uninstall = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
show: function () {
|
|
$scope.uninstall.error = {};
|
|
},
|
|
|
|
ask: function () {
|
|
$('#uninstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.uninstall.busy = true;
|
|
|
|
var NOOP = function (next) { return next(); };
|
|
var stopAppTask = $scope.app.taskId ? Client.stopTask.bind(null, $scope.app.taskId) : NOOP;
|
|
|
|
stopAppTask(function () { // ignore error
|
|
Client.uninstallApp($scope.app.id, function (error) {
|
|
if (error && error.statusCode === 402) { // unpurchase failed
|
|
Client.error('Relogin to Cloudron App Store');
|
|
} else if (error) {
|
|
Client.error(error);
|
|
} else {
|
|
$('#uninstallModal').modal('hide');
|
|
Client.refreshAppCache($scope.app.id, function() {}); // reflect the new app state immediately
|
|
$location.path('/apps');
|
|
}
|
|
|
|
$scope.uninstall.busy = false;
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.debug = {
|
|
show: function () {},
|
|
busyRunState: false,
|
|
|
|
stopAppTask: function (taskId) {
|
|
Client.stopTask(taskId, function (error) {
|
|
// we can ignore a call trying to cancel an already done task
|
|
if (error && error.statusCode !== 409) Client.error(error);
|
|
});
|
|
},
|
|
|
|
toggleRunState: function () {
|
|
var func = $scope.app.runState === RSTATES.STOPPED ? Client.startApp : Client.stopApp;
|
|
$scope.debug.busyRunState = true;
|
|
|
|
func($scope.app.id, function (error) {
|
|
$scope.debug.busyRunState = false;
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp();
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.restore = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
backup: null,
|
|
|
|
show: function (backup) {
|
|
$scope.restore.error = {};
|
|
$scope.restore.backup = backup;
|
|
|
|
$('#restoreModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.restore.busy = true;
|
|
|
|
Client.restoreApp($scope.app.id, $scope.restore.backup.id, function (error) {
|
|
if (error) {
|
|
Client.error(error);
|
|
$scope.restore.busy = false;
|
|
return;
|
|
}
|
|
|
|
$('#restoreModal').modal('hide');
|
|
|
|
refreshApp();
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.clone = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
backup: null,
|
|
location: '',
|
|
domain: null,
|
|
portBindings: {},
|
|
portBindingsInfo: {},
|
|
portBindingsEnabled: {},
|
|
|
|
show: function (backup) {
|
|
var app = $scope.app;
|
|
|
|
$scope.clone.error = {};
|
|
$scope.clone.backup = backup;
|
|
$scope.clone.domain = $scope.domains.find(function (d) { return app.domain === d.domain; }); // pre-select the app's domain
|
|
$scope.clone.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
|
// set default ports
|
|
for (var env in $scope.clone.portBindingsInfo) {
|
|
$scope.clone.portBindings[env] = $scope.clone.portBindingsInfo[env].defaultValue || 0;
|
|
$scope.clone.portBindingsEnabled[env] = true;
|
|
}
|
|
|
|
$('#cloneModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.clone.busy = true;
|
|
|
|
// only use enabled ports from portBindings
|
|
var finalPortBindings = {};
|
|
for (var env in $scope.clone.portBindings) {
|
|
if ($scope.clone.portBindingsEnabled[env]) {
|
|
finalPortBindings[env] = $scope.clone.portBindings[env];
|
|
}
|
|
}
|
|
|
|
var data = {
|
|
location: $scope.clone.location,
|
|
domain: $scope.clone.domain.domain,
|
|
portBindings: finalPortBindings,
|
|
backupId: $scope.clone.backup.id
|
|
};
|
|
|
|
Client.checkDNSRecords(data.domain, data.location, function (error, result) {
|
|
if (error) {
|
|
Client.error(error);
|
|
$scope.clone.busy = false;
|
|
return;
|
|
}
|
|
if (result.error) {
|
|
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
|
$scope.clone.error.location = 'DNS credentials for ' + data.domain + ' are invalid. Update it in Domains & Certs view';
|
|
} else {
|
|
$scope.clone.error.location = result.error.message;
|
|
}
|
|
$scope.clone.needsOverwrite = true;
|
|
$scope.clone.busy = false;
|
|
return;
|
|
}
|
|
if (result.needsOverwrite) {
|
|
$scope.clone.error.location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
|
$scope.clone.needsOverwrite = true;
|
|
$scope.clone.busy = false;
|
|
return;
|
|
}
|
|
|
|
Client.cloneApp($scope.app.id, data, function (error/*, clonedApp */) {
|
|
$scope.clone.busy = false;
|
|
|
|
if (error) {
|
|
if (error.statusCode === 409) {
|
|
if (error.portName) {
|
|
$scope.clone.error.port = error.message;
|
|
} else if (error.domain) {
|
|
$scope.clone.error.location = 'This location is already taken.';
|
|
$('#cloneLocationInput').focus();
|
|
} else {
|
|
Client.error(error);
|
|
}
|
|
} else {
|
|
Client.error(error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$('#cloneModal').modal('hide');
|
|
|
|
$location.path('/apps');
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.repair = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
location: null,
|
|
domain: null,
|
|
alternateDomains: [],
|
|
backups: [],
|
|
|
|
backupId: '',
|
|
|
|
// this prepares the repair dialog with whatever is required for repair action
|
|
show: function () {
|
|
$scope.repair.error = {};
|
|
$scope.repair.busy = false;
|
|
$scope.repair.location = null;
|
|
$scope.repair.domain = null;
|
|
$scope.repair.alternateDomains = [];
|
|
$scope.repair.backupId = '';
|
|
|
|
var app = $scope.app;
|
|
|
|
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
|
|
|
|
if (errorState === ISTATES.PENDING_LOCATION_CHANGE) {
|
|
$scope.repair.location = app.location;
|
|
$scope.repair.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
|
|
$scope.repair.alternateDomains = $scope.app.alternateDomains;
|
|
$scope.repair.alternateDomains = $scope.app.alternateDomains.map(function (altDomain) {
|
|
return {
|
|
subdomain: altDomain.subdomain,
|
|
enabled: true,
|
|
domain: $scope.domains.filter(function (d) { return d.domain === altDomain.domain; })[0]
|
|
};
|
|
});
|
|
}
|
|
|
|
if (errorState === ISTATES.PENDING_RESTORE) {
|
|
Client.getAppBackups($scope.app.id, function (error, backups) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.repair.backups = backups;
|
|
$scope.repair.backupId = '';
|
|
|
|
$('#repairModal').modal('show');
|
|
});
|
|
return;
|
|
}
|
|
|
|
$('#repairModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.repair.error = {};
|
|
$scope.repair.busy = true;
|
|
|
|
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
|
|
var data = {};
|
|
var repairFunc;
|
|
|
|
switch (errorState) {
|
|
case ISTATES.PENDING_INSTALL:
|
|
case ISTATES.PENDING_CLONE: // if manifest or bad image, use CLI to provide new manifest
|
|
repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
|
|
break;
|
|
|
|
case ISTATES.PENDING_LOCATION_CHANGE:
|
|
data.location = $scope.repair.location;
|
|
data.domain = $scope.repair.domain.domain;
|
|
data.alternateDomains = $scope.repair.alternateDomains.filter(function (a) { return a.enabled; })
|
|
.map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
|
|
data.overwriteDns = true; // always overwriteDns. user can anyway check and uncheck above
|
|
repairFunc = Client.configureApp.bind(null, $scope.app.id, 'location', data);
|
|
break;
|
|
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
|
repairFunc = Client.configureApp.bind(null, $scope.app.id, 'data_dir', { dataDir: null });
|
|
break;
|
|
|
|
// this also happens for import faliures. this UI can only show backup listing. use CLI for arbit id/config
|
|
case ISTATES.PENDING_RESTORE:
|
|
repairFunc = Client.restoreApp.bind(null, $scope.app.id, $scope.repair.backupId);
|
|
break;
|
|
|
|
case ISTATES.PENDING_UNINSTALL:
|
|
repairFunc = Client.uninstallApp.bind(null, $scope.app.id);
|
|
break;
|
|
|
|
case ISTATES.PENDING_START:
|
|
case ISTATES.PENDING_STOP:
|
|
case ISTATES.PENDING_RESIZE:
|
|
case ISTATES.PENDING_DEBUG:
|
|
case ISTATES.PENDING_RECREATE_CONTAINER:
|
|
case ISTATES.PENDING_CONFIGURE:
|
|
case ISTATES.PENDING_BACKUP: // only here for completeness, should never happen as this only set task error and not app erro
|
|
case ISTATES.PENDING_UPDATE: // when update failed, just bring it back to current state and user can click update again
|
|
default:
|
|
repairFunc = Client.repairApp.bind(null, $scope.app.id, {});
|
|
break;
|
|
}
|
|
|
|
repairFunc(function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.repair.busy = false;
|
|
$('#repairModal').modal('hide');
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.postInstallConfirm = {
|
|
message: '',
|
|
confirmed: false,
|
|
|
|
show: function () {
|
|
$scope.postInstallConfirm.message = $scope.app.manifest.postInstallMessage;
|
|
$scope.postInstallConfirm.confirmed = false;
|
|
|
|
$('#postInstallConfirmModal').modal('show');
|
|
|
|
return false; // prevent propagation and default
|
|
},
|
|
|
|
submit: function () {
|
|
if (!$scope.postInstallConfirm.confirmed) return;
|
|
|
|
$scope.app.pendingPostInstallConfirmation = false;
|
|
delete localStorage['confirmPostInstall_' + $scope.app.id];
|
|
|
|
$('#postInstallConfirmModal').modal('hide');
|
|
}
|
|
};
|
|
|
|
function fetchUsers(callback) {
|
|
Client.getUsers(function (error, users) {
|
|
if (error) return callback(error);
|
|
|
|
// ensure we have something to work with in the access restriction dropdowns
|
|
users.forEach(function (user) { user.display = user.username || user.email; });
|
|
|
|
$scope.users = users;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function fetchGroups(callback) {
|
|
Client.getGroups(function (error, groups) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.groups = groups;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function getDomains(callback) {
|
|
Client.getDomains(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.domains = result;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function getBackupConfig(callback) {
|
|
Client.getBackupConfig(function (error, backupConfig) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.backupEnabled = backupConfig.provider !== 'noop';
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function refreshApp(callback) {
|
|
callback = callback || function () {};
|
|
|
|
Client.getApp($scope.app.id, function (error, app) {
|
|
if (error && error.statusCode === 404) return $location.path('/apps');
|
|
if (error) return callback(error);
|
|
|
|
// Immediately move to debug view on error
|
|
if (app.error && !($scope.view === 'debug' || $scope.view === 'uninstall')) $scope.setView('debug');
|
|
|
|
// ensure we have amended progress properties set before copy
|
|
app.taskProgress = $scope.app.taskProgress;
|
|
app.taskProgressMessage = $scope.app.taskProgressMessage;
|
|
|
|
$scope.app = app;
|
|
|
|
if (app.taskId) {
|
|
Client.getTask(app.taskId, function (error, task) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.app.taskProgress = task && task.percent ? task.percent : 5; // start with 5 to avoid empty progress bar
|
|
$scope.app.taskProgressMessage = task ? task.message : '';
|
|
|
|
callback();
|
|
});
|
|
} else {
|
|
$scope.app.taskProgress = 0;
|
|
$scope.app.taskProgressMessage = '';
|
|
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
function waitForAppTask(callback) {
|
|
callback = callback || function () {};
|
|
|
|
if (!$scope.app.taskId) return callback();
|
|
|
|
// app will be refreshed on interval
|
|
$timeout(waitForAppTask.bind(null, callback), 2000); // not yet done
|
|
}
|
|
|
|
Client.onReady(function () {
|
|
Client.getApp(appId, function (error, app) {
|
|
if (error && error.statusCode === 404) return $location.path('/apps');
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.app = app;
|
|
|
|
$scope.setView($routeParams.view || 'display');
|
|
|
|
// track on page load backup if active
|
|
if (app.installationState === ISTATES.PENDING_BACKUP) $scope.backups.trackBackupTask();
|
|
|
|
asyncSeries([
|
|
fetchUsers,
|
|
fetchGroups,
|
|
getDomains,
|
|
getBackupConfig
|
|
], function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.display.show();
|
|
$scope.location.show();
|
|
$scope.resources.show();
|
|
$scope.access.show();
|
|
$scope.email.show();
|
|
$scope.security.show();
|
|
$scope.backups.show();
|
|
$scope.updates.show();
|
|
|
|
var refreshTimer = $interval(function () { refreshApp(); }, 2000); // call with inline function to avoid iteration argument passed see $interval docs
|
|
$scope.$on('$destroy', function () {
|
|
$interval.cancel(refreshTimer);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
$('#iconFileInput').get(0).onchange = function (event) {
|
|
var fr = new FileReader();
|
|
fr.onload = function () {
|
|
$scope.$apply(function () {
|
|
// var file = event.target.files[0];
|
|
$scope.display.icon.data = fr.result;
|
|
});
|
|
};
|
|
fr.readAsDataURL(event.target.files[0]);
|
|
};
|
|
|
|
// setup all the dialog focus handling
|
|
['appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
|
|
$('#' + id).on('shown.bs.modal', function () {
|
|
$(this).find('[autofocus]:first').focus();
|
|
});
|
|
});
|
|
|
|
$('.modal-backdrop').remove();
|
|
}]);
|