Initial commit

This commit is contained in:
Girish Ramakrishnan
2018-01-22 13:01:38 -08:00
commit 13e4ceff44
138 changed files with 56691 additions and 0 deletions

208
webadmin/src/js/appstore.js Normal file
View File

@@ -0,0 +1,208 @@
'use strict';
/* global angular:false */
angular.module('Application').service('AppStore', ['$http', '$base64', 'Client', function ($http, $base64, Client) {
function AppStoreError(statusCode, message) {
Error.call(this);
this.name = this.constructor.name;
this.statusCode = statusCode;
if (typeof message == 'string') {
this.message = message;
} else {
this.message = JSON.stringify(message);
}
}
function AppStore() {
this._appsCache = [];
}
AppStore.prototype.getApps = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var that = this;
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps', { params: { boxVersion: Client.getConfig().version } }).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
angular.copy(data.apps, that._appsCache);
return callback(null, that._appsCache);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppsFast = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
if (this._appsCache.length !== 0) return callback(null, this._appsCache);
this.getApps(callback);
};
AppStore.prototype.getAppById = function (appId, callback) {
var that = this;
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId) return callback(null, this._appsCache[app]);
}
this.getApps(function (error) {
if (error) return callback(error);
// recheck cache
for (var app in that._appsCache) {
if (that._appsCache[app].id === appId) return callback(null, that._appsCache[app]);
}
callback(new AppStoreError(404, 'Not found'));
});
};
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId && this._appsCache[app].manifest.version === version) return callback(null, this._appsCache[app]);
}
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppById = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// do not check cache, always get the latest
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var manifestUrl = Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId;
console.log('Getting the manifest of ', appId, manifestUrl);
$http.get(manifestUrl).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.manifest);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSizes = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/sizes').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.sizes);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getRegions = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/regions').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.regions);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.register = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/users', data).success(function (data, status) {
if (status !== 201) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.login = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password,
persistent: true
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/login', data).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.logout = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/logout').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getProfile = function (token, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/profile', { params: { accessToken: token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.profile);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getCloudronDetails = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId, { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.cloudron);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSubscription = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription', { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.subscription);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
return new AppStore();
}]);

1250
webadmin/src/js/client.js Normal file

File diff suppressed because it is too large Load Diff

499
webadmin/src/js/index.js Normal file
View File

@@ -0,0 +1,499 @@
'use strict';
/* global angular:false */
/* global showdown:false */
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
if (search.accessToken) {
localStorage.token = search.accessToken;
// strip the accessToken and expiresAt, then preserve the rest
delete search.accessToken;
delete search.expiresAt;
// this will reload the page as this is not a hash change
window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
}
// create main application module
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
app.config(['NotificationProvider', function (NotificationProvider) {
NotificationProvider.setOptions({
delay: 5000,
startTop: 60,
positionX: 'left',
maxCount: 3,
templateUrl: 'notification.html'
});
}]);
// setup all major application routes
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {
redirectTo: '/apps'
}).when('/users', {
controller: 'UsersController',
templateUrl: 'views/users.html'
}).when('/appstore', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html'
}).when('/appstore/:appId', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html'
}).when('/apps', {
controller: 'AppsController',
templateUrl: 'views/apps.html'
}).when('/account', {
controller: 'AccountController',
templateUrl: 'views/account.html'
}).when('/graphs', {
controller: 'GraphsController',
templateUrl: 'views/graphs.html'
}).when('/domains', {
controller: 'DomainsController',
templateUrl: 'views/domains.html'
}).when('/email', {
controller: 'EmailController',
templateUrl: 'views/email.html'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'
}).when('/activity', {
controller: 'ActivityController',
templateUrl: 'views/activity.html'
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
}).when('/tokens', {
controller: 'TokensController',
templateUrl: 'views/tokens.html'
}).otherwise({ redirectTo: '/'});
}]);
// keep in sync with appdb.js
var ISTATES = {
PENDING_INSTALL: 'pending_install',
PENDING_CLONE: 'pending_clone',
PENDING_CONFIGURE: 'pending_configure',
PENDING_UNINSTALL: 'pending_uninstall',
PENDING_RESTORE: 'pending_restore',
PENDING_UPDATE: 'pending_update',
PENDING_FORCE_UPDATE: 'pending_force_update',
PENDING_BACKUP: 'pending_backup',
ERROR: 'error',
INSTALLED: 'installed'
};
var HSTATES = {
HEALTHY: 'healthy',
UNHEALTHY: 'unhealthy',
ERROR: 'error',
DEAD: 'dead'
};
app.filter('installError', function () {
return function (app) {
if (app.installationState === ISTATES.ERROR) return true;
if (app.installationState === ISTATES.INSTALLED) {
// app.health can also be null to indicate insufficient data
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) return true;
}
return false;
};
});
app.filter('installSuccess', function () {
return function (app) {
return app.installationState === ISTATES.INSTALLED;
};
});
app.filter('activeOAuthClients', function () {
return function (clients, user) {
return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); });
};
});
app.filter('prettyAppMessage', function () {
return function (message) {
if (message === 'ETRYAGAIN') return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
if (message === 'DNS Record already exists') return 'The DNS record for this location already exists. Manually remove the DNS record and then click on repair.';
return message;
};
});
app.filter('shortAppMessage', function () {
return function (message) {
if (message === 'ETRYAGAIN') return 'DNS record not setup correctly';
return message;
};
});
app.filter('prettyMemory', function () {
return function (memory) {
// Adjust the default memory limit if it changes
return memory ? Math.floor(memory / 1024 / 1024) : 256;
};
});
app.filter('installationActive', function () {
return function(app) {
if (app.installationState === ISTATES.ERROR) return false;
if (app.installationState === ISTATES.INSTALLED) return false;
return true;
};
});
app.filter('installationStateLabel', function() {
// for better DNS errors
function detailedError(app) {
if (app.installationProgress === 'ETRYAGAIN') return 'DNS Error';
return 'Error';
}
return function(app) {
var waiting = app.progress === 0 ? ' (Pending)' : '';
switch (app.installationState) {
case ISTATES.PENDING_INSTALL:
case ISTATES.PENDING_CLONE:
return 'Installing' + waiting;
case ISTATES.PENDING_CONFIGURE: return 'Configuring' + waiting;
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_FORCE_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
case ISTATES.ERROR: return detailedError(app);
case ISTATES.INSTALLED: {
if (app.runState === 'running') {
if (!app.health) return 'Starting...'; // no data yet
if (app.health === HSTATES.HEALTHY) return 'Running';
return 'Not responding'; // dead/exit/unhealthy
} else if (app.runState === 'pending_start') return 'Starting...';
else if (app.runState === 'pending_stop') return 'Stopping...';
else if (app.runState === 'stopped') return 'Stopped';
else return app.runState;
break;
}
default: return app.installationState;
}
};
});
app.filter('readyToUpdate', function () {
return function (apps) {
return apps.every(function (app) {
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
});
};
});
app.filter('inProgressApps', function () {
return function (apps) {
return apps.filter(function (app) {
return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
});
};
});
app.filter('ignoreAdminGroup', function () {
return function (groups) {
return groups.filter(function (group) {
if (group.id) return group.id !== 'admin';
return group !== 'admin';
});
};
});
app.filter('applicationLink', function() {
return function(app) {
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY) {
return 'https://' + app.fqdn;
} else {
return '';
}
};
});
app.filter('prettyHref', function () {
return function (input) {
if (!input) return input;
if (input.indexOf('http://') === 0) return input.slice('http://'.length);
if (input.indexOf('https://') === 0) return input.slice('https://'.length);
return input;
};
});
app.filter('prettyDate', function () {
// http://ejohn.org/files/pretty.js
return function prettyDate(time) {
var date = new Date(time),
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 'just now';
return day_diff === 0 && (
diff < 60 && 'just now' ||
diff < 120 && '1 minute ago' ||
diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
diff < 7200 && '1 hour ago' ||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
day_diff === 1 && 'Yesterday' ||
day_diff < 7 && day_diff + ' days ago' ||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
Math.round( day_diff / 365 ) + ' years ago';
};
});
app.filter('markdown2html', function () {
var converter = new showdown.Converter({
extensions: ['targetblank'],
simplifiedAutoLink: true,
strikethrough: true,
tables: true
});
return function (text) {
return converter.makeHtml(text);
};
});
app.filter('postInstallMessage', function () {
var SSO_MARKER = '=== sso ===';
return function (text, app) {
if (!text) return '';
if (!app) return text;
var parts = text.split(SSO_MARKER);
if (parts.length === 1) {
// [^] matches even newlines. '?' makes it non-greedy
if (app.sso) return text.replace(/\<nosso\>[^]*?\<\/nosso\>/g, '');
else return text.replace(/\<sso\>[^]*?\<\/sso\>/g, '');
}
if (app.sso) return parts[1];
else return parts[0];
};
});
// keep this in sync with eventlog.js and CLI tool
var ACTION_ACTIVATE = 'cloudron.activate';
var ACTION_APP_CONFIGURE = 'app.configure';
var ACTION_APP_INSTALL = 'app.install';
var ACTION_APP_RESTORE = 'app.restore';
var ACTION_APP_UNINSTALL = 'app.uninstall';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_LOGIN = 'app.login';
var ACTION_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_BACKUP_CLEANUP = 'backup.cleanup';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
var ACTION_CLI_MODE = 'settings.climode';
var ACTION_START = 'cloudron.start';
var ACTION_UPDATE = 'cloudron.update';
var ACTION_USER_ADD = 'user.add';
var ACTION_USER_LOGIN = 'user.login';
var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
app.filter('eventLogDetails', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
switch (eventLog.action) {
case ACTION_ACTIVATE: return 'Cloudron activated';
case ACTION_APP_CONFIGURE: return 'App ' + data.appId + ' was configured';
case ACTION_APP_INSTALL: return 'App ' + data.manifest.id + '@' + data.manifest.version + ' installed at ' + data.location + ' with id ' + data.appId;
case ACTION_APP_RESTORE: return 'App ' + data.appId + ' restored';
case ACTION_APP_UNINSTALL: return 'App ' + data.appId + ' uninstalled';
case ACTION_APP_UPDATE: return 'App ' + data.appId + ' updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
case ACTION_APP_LOGIN: return 'App ' + data.appId + ' logged in';
case ACTION_BACKUP_START: return 'Backup started';
case ACTION_BACKUP_FINISH: return 'Backup finished. ' + (errorMessage ? ('error: ' + errorMessage) : ('id: ' + data.filename));
case ACTION_BACKUP_CLEANUP: return 'Backup ' + data.backup.id + ' removed';
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : 'succeeded');
case ACTION_CLI_MODE: return 'CLI mode was ' + (data.enabled ? 'enabled' : 'disabled');
case ACTION_START: return 'Cloudron started with version ' + data.version;
case ACTION_UPDATE: return 'Updating to version ' + data.boxUpdateInfo.version;
case ACTION_USER_ADD: return 'User ' + data.email + ' added with id ' + data.userId;
case ACTION_USER_LOGIN: return 'User ' + data.userId + ' logged in';
case ACTION_USER_REMOVE: return 'User ' + data.userId + ' removed';
case ACTION_USER_UPDATE: return 'User ' + data.userId + ' updated';
default: return eventLog.action;
}
};
});
// custom directive for dynamic names in forms
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
app.directive('laterName', function () { // (2)
return {
restrict: 'A',
require: ['?ngModel', '^?form'], // (3)
link: function postLink(scope, elem, attrs, ctrls) {
attrs.$set('name', attrs.laterName);
var modelCtrl = ctrls[0]; // (3)
var formCtrl = ctrls[1]; // (3)
if (modelCtrl && formCtrl) {
modelCtrl.$name = attrs.name; // (4)
formCtrl.$addControl(modelCtrl); // (2)
scope.$on('$destroy', function () {
formCtrl.$removeControl(modelCtrl); // (5)
});
}
}
};
});
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
var original = $location.path;
$location.path = function (path, reload) {
if (reload === false) {
var lastRoute = $route.current;
var un = $rootScope.$on('$locationChangeSuccess', function () {
$route.current = lastRoute;
un();
});
}
return original.apply($location, [path]);
};
}]);
app.directive('ngClickSelect', function () {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
element.bind('click', function () {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(this);
selection.removeAllRanges();
selection.addRange(range);
});
}
};
});
app.directive('ngClickReveal', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.addClass('hand');
var value = '';
scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) {
if (newValue !== oldValue) {
element.html('<i>hidden</i>');
value = newValue;
}
});
element.bind('click', function () {
element.text(value);
});
}
};
});
// https://codepen.io/webmatze/pen/isuHh
app.directive('tagInput', function () {
return {
restrict: 'E',
scope: {
inputTags: '=taglist'
},
link: function ($scope, element, attrs) {
$scope.defaultWidth = 200;
$scope.tagText = ''; // current tag being edited
$scope.placeholder = attrs.placeholder;
$scope.tagArray = function () {
if ($scope.inputTags === undefined) {
return [];
}
return $scope.inputTags.split(',').filter(function (tag) {
return tag !== '';
});
};
$scope.addTag = function () {
var tagArray;
if ($scope.tagText.length === 0) {
return;
}
tagArray = $scope.tagArray();
tagArray.push($scope.tagText);
$scope.inputTags = tagArray.join(',');
return $scope.tagText = '';
};
$scope.deleteTag = function (key) {
var tagArray;
tagArray = $scope.tagArray();
if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
tagArray.pop();
} else {
if (key !== undefined) {
tagArray.splice(key, 1);
}
}
return $scope.inputTags = tagArray.join(',');
};
$scope.$watch('tagText', function (newVal, oldVal) {
var tempEl;
if (!(newVal === oldVal && newVal === undefined)) {
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
$scope.inputWidth = tempEl.width() + 5;
if ($scope.inputWidth < $scope.defaultWidth) {
$scope.inputWidth = $scope.defaultWidth;
}
return tempEl.remove();
}
});
element.bind('keydown', function (e) {
var key = e.which;
if (key === 9 || key === 13) {
e.preventDefault();
}
if (key === 8) {
return $scope.$apply('deleteTag()');
}
});
element.bind('keyup', function (e) {
var key = e.which;
if (key === 9 || key === 13 || key === 32 || key === 188) {
e.preventDefault();
return $scope.$apply('addTag()');
}
});
},
template:
'<div class="tag-input-container">' +
'<div class="input-tag" data-ng-repeat="tag in tagArray()">' +
'{{tag}}' +
'<div class="delete-tag" data-ng-click="deleteTag($index)">&times;</div>' +
'</div>' +
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
'</div>'
};
});
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
fitTextConfigProvider.config = {
loadDelay: 250,
compressor: 0.9,
min: 8,
max: 24
};
}]);

134
webadmin/src/js/logs.js Normal file
View File

@@ -0,0 +1,134 @@
'use strict';
/* global moment */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $timeout, $location, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.initialized = false;
$scope.installedApps = Client.getInstalledApps();
$scope.client = Client;
$scope.logs = [];
$scope.selected = '';
$scope.activeEventSource = null;
$scope.lines = 10;
$scope.selectedAppInfo = null;
// Add built-in log types for now
$scope.logs.push({ name: 'System (All)', type: 'platform', value: 'all', url: Client.makeURL('/api/v1/cloudron/logs?units=all') });
$scope.logs.push({ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs?units=box') });
$scope.logs.push({ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs?units=mail') });
$scope.error = function (error) {
console.error(error);
window.location.href = '/error.html';
};
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
$scope.clear = function () {
var logViewer = $('.logs-container');
logViewer.empty();
};
$scope.showTerminal = function () {
if (!$scope.selected) return;
window.open('/terminal.html?id=' + $scope.selected.value, 'Cloudron Terminal', 'width=1024,height=800');
};
function showLogs() {
if (!$scope.selected) return;
var func = $scope.selected.type === 'platform' ? Client.getPlatformLogs : Client.getAppLogs;
func($scope.selected.value, true, $scope.lines, function handleLogs(error, result) {
if (error) return console.error(error);
$scope.activeEventSource = result;
result.onmessage = function handleMessage(message) {
var data;
try {
data = JSON.parse(message.data);
} catch (e) {
return console.error(e);
}
// check if we want to auto scroll (this is before the appending, as that skews the check)
var tmp = $('.logs-container');
var autoScroll = tmp[0].scrollTop > (tmp[0].scrollTopMax - 24);
var logLine = $('<div class="log-line">');
var timeString = moment.utc(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss');
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(typeof data.message === 'string' ? data.message : ab2str(data.message)));
tmp.append(logLine);
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
};
});
}
Client.onApps(function () {
if ($scope.selected.type !== 'app') return;
var appId = $scope.selected.value;
Client.getApp(appId, function (error, result) {
if (error) return console.error(error);
$scope.selectedAppInfo = result;
});
});
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
}
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = Client.getConfig().version;
} else if (localStorage.version !== Client.getConfig().version) {
localStorage.version = Client.getConfig().version;
window.location.reload(true);
}
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
Client.getInstalledApps().forEach(function (app) {
$scope.logs.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
});
});
// activate pre-selected log from query otherwise choose the first one
$scope.selected = $scope.logs.find(function (e) { return e.value === search.id; });
if (!$scope.selected) $scope.selected = $scope.logs[0];
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showLogs();
});
});
});
}]);

258
webadmin/src/js/main.js Normal file
View File

@@ -0,0 +1,258 @@
'use strict';
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', 'AppStore', function ($scope, $route, $timeout, $location, Client, AppStore) {
$scope.initialized = false;
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.status = {};
$scope.client = Client;
$scope.currentSubscription = null;
$scope.appstoreConfig = {};
$scope.hideNavBarActions = $location.path() === '/logs';
$scope.update = {
busy: false,
error: {}
};
$scope.isActive = function (url) {
if (!$route.current) return false;
return $route.current.$$route.originalPath.indexOf(url) === 0;
};
$scope.logout = function (event) {
event.stopPropagation();
$scope.initialized = false;
Client.logout();
};
$scope.error = function (error) {
console.error(error);
window.location.href = '/error.html';
};
$scope.waitingForPlanSelection = false;
$('#setupSubscriptionModal').on('hide.bs.modal', function () {
$scope.waitingForPlanSelection = false;
// check for updates to stay in sync
Client.checkForUpdates(function (error) {
if (error) return console.error(error);
Client.refreshConfig();
});
});
$scope.waitForPlanSelection = function () {
if ($scope.waitingForPlanSelection) return;
$scope.waitingForPlanSelection = true;
function checkPlan() {
if (!$scope.waitingForPlanSelection) return;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
$scope.currentSubscription = result;
// check again to give more immediate feedback once a subscription was setup
if (result.plan.id === 'undecided' || result.plan.id === 'free') {
$timeout(checkPlan, 5000);
} else {
$scope.waitingForPlanSelection = false;
$('#setupSubscriptionModal').modal('hide');
if ($scope.config.update && $scope.config.update.box) $('#updateModal').modal('show');
}
});
}
checkPlan();
};
$scope.showSubscriptionModal = function () {
$('#setupSubscriptionModal').modal('show');
};
$scope.showUpdateModal = function (form) {
$scope.update.error.generic = null;
form.$setPristine();
form.$setUntouched();
if (!$scope.config.update.box.sourceTarballUrl) {
// no sourceTarballUrl means we can't update here this is only from 1.0 on
// this will also handle the 'undecided' and 'free' plan, since the server does not send the url in this case
$('#setupSubscriptionModal').modal('show');
} else if ($scope.config.provider === 'caas') {
$('#updateModal').modal('show');
} else if (!$scope.currentSubscription || !$scope.currentSubscription.plan) {
// do nothing as we were not able to get a subscription, yet
} else {
$('#updateModal').modal('show');
}
};
$scope.doUpdate = function () {
$scope.update.error.generic = null;
$scope.update.busy = true;
Client.update(function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.update.error.generic = 'Please try again later. The Cloudron is creating a backup at the moment.';
} else {
$scope.update.error.generic = error.message;
console.error('Unable to update.', error);
}
$scope.update.busy = false;
return;
}
window.location.href = '/update.html';
});
};
function runConfigurationChecks() {
var actionScope;
// warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active')
if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) {
actionScope = $scope.$new(true);
actionScope.action = '/#/certs';
Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', actionScope);
}
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
if (backupConfig.provider === 'noop') {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
Client.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope);
}
Client.getMailRelay(function (error, result) {
if (error) return console.error(error);
// the email status checks are currently only useful when using Cloudron itself for relaying
if (result.provider !== 'cloudron-smtp') return;
// Check if all email DNS records are set up properly only for non external DNS API
Client.getEmailStatus(function (error, result) {
if (error) return console.error(error);
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/email';
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
}
});
});
});
}
$scope.getSubscription = function () {
Client.getAppstoreConfig(function (error, result) {
if (error) return console.error(error);
if (result.token) {
$scope.appstoreConfig = result;
AppStore.getProfile(result.token, function (error, result) {
if (error) return console.error(error);
$scope.appstoreConfig.profile = result;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
$scope.currentSubscription = result;
});
});
}
});
}
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
// WARNING if anything about the routing is changed here test these use-cases:
//
// 1. Caas
// 2. selfhosted with --domain argument
// 3. selfhosted restore
// 4. local development with gulp develop
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
return;
}
// support local development with localhost check
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
// user is accessing by IP or by the old admin location (pre-migration)
window.location.href = '/setupdns.html';
return;
}
$scope.status = status;
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = Client.getConfig().version;
} else if (localStorage.version !== Client.getConfig().version) {
localStorage.version = Client.getConfig().version;
window.location.reload(true);
}
Client.refreshUserInfo(function (error) {
if (error) return $scope.error(error);
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
$scope.initialized = true;
if ($scope.user.admin) {
runConfigurationChecks();
if ($scope.config.provider !== 'caas') $scope.getSubscription();
}
});
});
});
});
Client.onConfig(function (config) {
// check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
// setup all the dialog focus handling
['updateModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
}]);

237
webadmin/src/js/restore.js Normal file
View File

@@ -0,0 +1,237 @@
'use strict';
/* global tld */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.filter('zoneName', function () {
return function (domain) {
return tld.getDomain(domain);
};
});
app.controller('RestoreController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.busy = false;
$scope.error = {};
$scope.provider = '';
$scope.bucket = '';
$scope.prefix = '';
$scope.accessKeyId = '';
$scope.secretAccessKey = '';
$scope.gcsKey = { keyFileName: '', content: '' };
$scope.region = '';
$scope.endpoint = '';
$scope.backupFolder = '';
$scope.backupId = '';
$scope.instanceId = '';
$scope.acceptSelfSignedCerts = false;
$scope.format = 'tgz';
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
{ name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
{ name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' },
{ name: 'Canada (Central)', value: 'ca-central-1' },
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
{ name: 'EU (Ireland)', value: 'eu-west-1' },
{ name: 'EU (London)', value: 'eu-west-2' },
{ name: 'South America (São Paulo)', value: 'sa-east-1' },
{ name: 'US East (N. Virginia)', value: 'us-east-1' },
{ name: 'US East (Ohio)', value: 'us-east-2' },
{ name: 'US West (N. California)', value: 'us-west-1' },
{ name: 'US West (Oregon)', value: 'us-west-2' },
];
$scope.doSpacesRegions = [
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' }
];
$scope.storageProvider = [
{ name: 'Amazon S3', value: 's3' },
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Google Cloud Storage', value: 'gcs' },
{ name: 'Minio', value: 'minio' },
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
];
$scope.formats = [
{ name: 'Tarball (zipped)', value: 'tgz' },
{ name: 'rsync', value: 'rsync' }
];
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces';
};
$scope.restore = function () {
$scope.error = {};
$scope.busy = true;
var backupConfig = {
provider: $scope.provider,
key: $scope.key,
format: $scope.format
};
// only set provider specific fields, this will clear them in the db
if ($scope.s3like(backupConfig.provider)) {
backupConfig.bucket = $scope.bucket;
backupConfig.prefix = $scope.prefix;
backupConfig.accessKeyId = $scope.accessKeyId;
backupConfig.secretAccessKey = $scope.secretAccessKey;
if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint;
if (backupConfig.provider === 's3') {
if ($scope.region) backupConfig.region = $scope.region;
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
backupConfig.region = 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts;
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.endpoint = 'https://sos.exo.io';
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v2';
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
}
} else if (backupConfig.provider === 'gcs') {
backupConfig.bucket = $scope.bucket;
backupConfig.prefix = $scope.prefix;
try {
var serviceAccountKey = JSON.parse($scope.gcsKey.content);
backupConfig.projectId = serviceAccountKey.project_id;
backupConfig.credentials = {
client_email: serviceAccountKey.client_email,
private_key: serviceAccountKey.private_key
};
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
throw 'fields_missing';
}
} catch (e) {
$scope.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
$scope.error.gcsKeyInput = true;
$scope.busy = false;
return;
}
} else if (backupConfig.provider === 'filesystem') {
backupConfig.backupFolder = $scope.backupFolder;
}
if ($scope.backupId.indexOf('/') === -1) {
$scope.error.generic = 'Backup id must include the directory path';
$scope.error.backupId = true;
$scope.busy = false;
return;
}
var version = $scope.backupId.match(/_v(\d+.\d+.\d+)/);
if (!version) {
$scope.error.generic = 'Backup id is missing version information';
$scope.error.backupId = true;
$scope.busy = false;
return;
}
Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', function (error) {
$scope.busy = false;
if (error) {
if (error.statusCode === 402) {
$scope.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
$scope.error.accessKeyId = true;
$scope.accessKeyId = '';
$scope.configureBackupForm.accessKeyId.$setPristine();
$('#inputConfigureBackupAccessKeyId').focus();
} else if (error.message.indexOf('not match the signature') !== -1 ) {
$scope.error.secretAccessKey = true;
$scope.secretAccessKey = '';
$scope.configureBackupForm.secretAccessKey.$setPristine();
$('#inputConfigureBackupSecretAccessKey').focus();
} else if (error.message.toLowerCase() === 'access denied') {
$scope.error.bucket = true;
$scope.bucket = '';
$scope.configureBackupForm.bucket.$setPristine();
$('#inputConfigureBackupBucket').focus();
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
$scope.error.generic = 'Unknown region';
$scope.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupRegion').focus();
} else if (error.message.toLowerCase() === 'wrong region') {
$scope.error.generic = 'Wrong S3 Region';
$scope.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupRegion').focus();
} else {
$('#inputConfigureBackupBucket').focus();
}
} else {
$scope.error.generic = error.message;
}
return;
}
waitForRestore();
});
}
function waitForRestore() {
$scope.busy = true;
Client.getStatus(function (error, status) {
if (!error && !status.webadminStatus.restoring) {
window.location.href = '/';
}
setTimeout(waitForRestore, 5000);
});
}
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
Client.getStatus(function (error, status) {
if (error) {
window.location.href = '/error.html';
return;
}
if (status.restoring) return waitForRestore();
if (status.activated) {
window.location.href = '/';
return;
}
$scope.instanceId = search.instanceId;
$scope.initialized = true;
});
}]);

97
webadmin/src/js/setup.js Normal file
View File

@@ -0,0 +1,97 @@
'use strict';
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.initialized = false;
$scope.busy = false;
$scope.account = {
email: '',
displayName: '',
requireEmail: false,
username: '',
password: ''
};
$scope.error = null;
$scope.provider = '';
$scope.apiServerOrigin = '';
$scope.setupToken = '';
$scope.activateCloudron = function () {
$scope.busy = true;
$scope.error = null;
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, function (error) {
if (error && error.statusCode === 400) {
$scope.busy = false;
$scope.error = { username: error.message };
$scope.account.username = '';
$scope.setupForm.username.$setPristine();
setTimeout(function () { $('#inputUsername').focus(); }, 200);
return;
} else if (error) {
$scope.busy = false;
console.error('Internal error', error);
$scope.error = { generic: error.message };
return;
}
window.location.href = '/';
});
};
Client.getStatus(function (error, status) {
if (error) {
window.location.href = '/error.html';
return;
}
// if we are here from the ip first go to the real domain if already setup
if (status.provider !== 'caas' && status.adminFqdn && status.adminFqdn !== window.location.hostname) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
return;
}
// if we don't have a domain yet, first go to domain setup
if (!status.adminFqdn) {
window.location.href = '/setupdns.html';
return;
}
if (status.activated) {
window.location.href = '/';
return;
}
if (status.provider === 'caas') {
if (!search.setupToken) {
window.location.href = '/error.html?errorCode=2';
return;
}
if (!search.email) {
window.location.href = '/error.html?errorCode=3';
return;
}
$scope.setupToken = search.setupToken;
}
$scope.account.email = search.email || $scope.account.email;
$scope.account.displayName = search.displayName || $scope.account.displayName;
$scope.account.requireEmail = !search.email;
$scope.provider = status.provider;
$scope.apiServerOrigin = status.apiServerOrigin;
$scope.initialized = true;
// Ensure we have a good autofocus
setTimeout(function () {
$(document).find("[autofocus]:first").focus();
}, 250);
});
}]);

182
webadmin/src/js/setupdns.js Normal file
View File

@@ -0,0 +1,182 @@
'use strict';
/* global tld */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.filter('zoneName', function () {
return function (domain) {
return tld.getDomain(domain);
};
});
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
$scope.error = null;
$scope.provider = '';
$scope.showDNSSetup = false;
$scope.instanceId = '';
$scope.explicitZone = search.zone || '';
$scope.isEnterprise = !!search.enterprise;
$scope.isDomain = false;
$scope.isSubdomain = false;
// If we migrate the api origin we have to poll the new location
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
$scope.$watch('dnsCredentials.domain', function (newVal) {
if (!newVal) {
$scope.isDomain = false;
$scope.isSubdomain = false;
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
$scope.isDomain = false;
$scope.isSubdomain = false;
} else {
$scope.isDomain = true;
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
}
});
// keep in sync with domains.js
$scope.dnsProvider = [
{ name: 'AWS Route53', value: 'route53' },
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
{ name: 'Digital Ocean', value: 'digitalocean' },
{ name: 'Google Cloud DNS', value: 'gcdns' },
{ name: 'Wildcard', value: 'wildcard' },
{ name: 'Manual (not recommended)', value: 'manual' },
{ name: 'No-op (only for development)', value: 'noop' }
];
$scope.dnsCredentials = {
error: null,
busy: false,
domain: '',
accessKeyId: '',
secretAccessKey: '',
gcdnsKey: { keyFileName: '', content: '' },
digitalOceanToken: '',
provider: 'route53'
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
$scope.setDnsCredentials = function () {
$scope.dnsCredentials.busy = true;
$scope.dnsCredentials.error = null;
$scope.error = null;
var provider = $scope.dnsCredentials.provider;
var data = {
providerToken: $scope.instanceId
};
// special case the wildcard provider
if (provider === 'wildcard') {
provider = 'manual';
data.wildcard = true;
}
if (provider === 'route53') {
data.accessKeyId = $scope.dnsCredentials.accessKeyId;
data.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
} else if (provider === 'gcdns'){
try {
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
data.projectId = serviceAccountKey.project_id;
data.credentials = {
client_email: serviceAccountKey.client_email,
private_key: serviceAccountKey.private_key
};
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
throw 'fields_missing';
}
} catch(e) {
$scope.dnsCredentials.error = 'Cannot parse Google Service Account Key';
$scope.dnsCredentials.busy = false;
return;
}
} else if (provider === 'digitalocean') {
data.token = $scope.dnsCredentials.digitalOceanToken;
} else if (provider === 'cloudflare') {
data.email = $scope.dnsCredentials.cloudflareEmail;
data.token = $scope.dnsCredentials.cloudflareToken;
}
Client.setupDnsConfig($scope.dnsCredentials.domain, $scope.explicitZone, provider, data, function (error) {
if (error && error.statusCode === 403) {
$scope.dnsCredentials.busy = false;
$scope.error = 'Wrong instance id provided.';
return;
} else if (error) {
$scope.dnsCredentials.busy = false;
$scope.dnsCredentials.error = error.message;
return;
}
waitForDnsSetup();
});
};
function waitForDnsSetup() {
$scope.state = 'waitingForDnsSetup';
Client.getStatus(function (error, status) {
// webadminStatus.dns is intentionally not tested. it can be false if dns creds are invalid
// runConfigurationChecks() in main.js will pick the .dns and show a notification
if (!error && status.adminFqdn && status.webadminStatus.tls) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
}
setTimeout(waitForDnsSetup, 5000);
});
}
function initialize() {
Client.getStatus(function (error, status) {
if (error) {
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
console.error(error);
$scope.state = 'waitingForBox';
return $timeout(initialize, 3000);
}
// domain is currently like a lock flag
if (status.adminFqdn) return waitForDnsSetup();
if (status.provider === 'digitalocean') $scope.dnsCredentials.provider = 'digitalocean';
if (status.provider === 'gcp') $scope.dnsCredentials.provider = 'gcdns';
if (status.provider === 'ami') {
// remove route53 on ami
$scope.dnsProvider.shift();
$scope.dnsCredentials.provider = 'wildcard';
}
$scope.instanceId = search.instanceId;
$scope.provider = status.provider;
$scope.state = 'initialized';
});
}
initialize();
}]);

406
webadmin/src/js/terminal.js Normal file
View File

@@ -0,0 +1,406 @@
'use strict';
/* global Terminal */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $timeout, $location, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.apps = [];
$scope.selected = '';
$scope.terminal = null;
$scope.terminalSocket = null;
$scope.restartAppBusy = false;
$scope.appBusy = false;
$scope.selectedAppInfo = null;
$scope.downloadFile = {
error: '',
filePath: '',
busy: false,
downloadUrl: function () {
if (!$scope.downloadFile.filePath) return '';
var filePath = $scope.downloadFile.filePath.replace(/\/*\//g, '/');
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
},
show: function () {
$scope.downloadFile.busy = false;
$scope.downloadFile.error = '';
$scope.downloadFile.filePath = '';
$('#downloadFileModal').modal('show');
},
submit: function () {
$scope.downloadFile.busy = true;
Client.checkDownloadableFile($scope.selected.value, $scope.downloadFile.filePath, function (error) {
$scope.downloadFile.busy = false;
if (error) {
$scope.downloadFile.error = 'The requested file does not exist.';
return;
}
// we have to click the link to make the browser do the download
// don't know how to prevent the browsers
$('#fileDownloadLink')[0].click();
$('#downloadFileModal').modal('hide');
});
}
};
$scope.uploadProgress = {
busy: false,
total: 0,
current: 0,
show: function () {
$scope.uploadProgress.total = 0;
$scope.uploadProgress.current = 0;
$('#uploadProgressModal').modal('show');
},
hide: function () {
$('#uploadProgressModal').modal('hide');
}
};
$scope.uploadFile = function () {
var fileUpload = document.querySelector('#fileUpload');
fileUpload.onchange = function (e) {
if (e.target.files.length === 0) return;
$scope.uploadProgress.busy = true;
$scope.uploadProgress.show();
Client.uploadFile($scope.selected.value, e.target.files[0], function progress(e) {
$scope.uploadProgress.total = e.total;
$scope.uploadProgress.current = e.loaded;
}, function (error) {
if (error) console.error(error);
$scope.uploadProgress.busy = false;
$scope.uploadProgress.hide();
});
};
fileUpload.click();
};
$scope.populateDropdown = function () {
Client.getInstalledApps().forEach(function (app) {
$scope.apps.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons
});
});
// $scope.selected = $scope.apps[0];
};
$scope.usesAddon = function (addon) {
if (!$scope.selected || !$scope.selected.addons) return false;
return !!Object.keys($scope.selected.addons).find(function (a) { return a === addon; });
};
function reset() {
if ($scope.terminal) {
$scope.terminal.destroy();
$scope.terminal = null;
}
if ($scope.terminalSocket) {
$scope.terminalSocket = null;
}
$scope.selectedAppInfo = null;
}
$scope.restartApp = function () {
$scope.restartAppBusy = true;
var appId = $scope.selected.value;
function waitUntilStopped(callback) {
Client.refreshInstalledApps(function (error) {
if (error) return callback(error);
Client.getApp(appId, function (error, result) {
if (error) return callback(error);
if (result.runState === 'stopped') return callback();
setTimeout(waitUntilStopped.bind(null, callback), 2000);
});
});
}
Client.stopApp(appId, function (error) {
if (error) return console.error('Failed to stop app.', error);
waitUntilStopped(function (error) {
if (error) return console.error('Failed to get app status.', error);
Client.startApp(appId, function (error) {
if (error) console.error('Failed to start app.', error);
$scope.restartAppBusy = false;
});
});
});
};
$scope.repairApp = function () {
$('#repairAppModal').modal('show');
};
$scope.repairAppBegin = function () {
$scope.appBusy = true;
function waitUntilInRepairState() {
Client.refreshInstalledApps(function (error) {
if (error) return console.error('Failed to refresh app status.', error);
Client.getApp($scope.selected.value, function (error, result) {
if (error) return console.error('Failed to get app status.', error);
if (result.installationState === 'installed') $scope.appBusy = false;
else setTimeout(waitUntilInRepairState, 2000);
});
});
}
Client.debugApp($scope.selected.value, true, function (error) {
if (error) return console.error(error);
Client.refreshInstalledApps(function (error) {
if (error) console.error(error);
$('#repairAppModal').modal('hide');
waitUntilInRepairState();
});
});
};
$scope.repairAppDone = function () {
$scope.appBusy = true;
function waitUntilInNormalState() {
Client.refreshInstalledApps(function (error) {
if (error) return console.error('Failed to refresh app status.', error);
Client.getApp($scope.selected.value, function (error, result) {
if (error) return console.error('Failed to get app status.', error);
if (result.installationState === 'installed') $scope.appBusy = false;
else setTimeout(waitUntilInNormalState, 2000);
});
});
}
Client.debugApp($scope.selected.value, false, function (error) {
if (error) return console.error(error);
Client.refreshInstalledApps(function (error) {
if (error) console.error(error);
waitUntilInNormalState();
});
});
};
function showTerminal(retry) {
reset();
if (!$scope.selected) return;
var appId = $scope.selected.value;
Client.getApp(appId, function (error, result) {
if (error) return console.error(error);
// we expect this to be called _after_ a reconfigure was issued
if (result.installationState === 'pending_configure') {
$scope.appBusy = true;
} else if (result.installationState === 'installed') {
$scope.appBusy = false;
}
$scope.selectedAppInfo = result;
$scope.terminal = new Terminal();
$scope.terminal.open(document.querySelector('#terminalContainer'));
$scope.terminal.fit();
try {
// websocket cannot use relative urls
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
$scope.terminalSocket = new WebSocket(url);
$scope.terminal.attach($scope.terminalSocket);
$scope.terminalSocket.onclose = function () {
// retry in one second
$scope.terminalReconnectTimeout = setTimeout(function () {
showTerminal(true);
}, 1000);
};
// Let the browser handle paste
$scope.terminal.attachCustomKeyEventHandler(function (e) {
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) return false;
});
} catch (e) {
console.error(e);
}
if (retry) $scope.terminal.writeln('Reconnecting...');
else $scope.terminal.writeln('Connecting...');
});
}
$scope.terminalInject = function (addon) {
if (!$scope.terminalSocket) return;
var cmd;
if (addon === 'mysql') cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
else if (addon === 'postgresql') cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
else if (addon === 'mongodb') cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
else if (addon === 'redis') cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
if (!cmd) return;
cmd += ' ';
$scope.terminalSocket.send(cmd);
$scope.terminal.focus();
};
Client.onReady($scope.populateDropdown);
// Client.onApps(function () {
// console.log('onapps')
// if ($scope.$$destroyed) return;
// if ($scope.selected.type !== 'app') return $scope.appBusy = false;
// var appId = $scope.selected.value;
// Client.getApp(appId, function (error, result) {
// if (error) return console.error(error);
// // we expect this to be called _after_ a reconfigure was issued
// if (result.installationState === 'pending_configure') {
// $scope.appBusy = true;
// } else if (result.installationState === 'installed') {
// $scope.appBusy = false;
// }
// $scope.selectedAppInfo = result;
// });
// });
// terminal right click handling
$scope.terminalClear = function () {
if (!$scope.terminal) return;
$scope.terminal.clear();
$scope.terminal.focus();
};
$scope.terminalCopy = function () {
if (!$scope.terminal) return;
// execCommand('copy') would copy any selection from the page, so do this only if terminal has a selection
if (!$scope.terminal.getSelection()) return;
document.execCommand('copy');
$scope.terminal.focus();
};
$('.contextMenuBackdrop').on('click', function (e) {
$('#terminalContextMenu').hide();
$('.contextMenuBackdrop').hide();
$scope.terminal.focus();
});
$('#terminalContainer').on('contextmenu', function (e) {
if (!$scope.terminal) return true;
e.preventDefault();
$('.contextMenuBackdrop').show();
$('#terminalContextMenu').css({
display: 'block',
left: e.pageX,
top: e.pageY
});
return false;
});
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
if (!status.activated) {
console.log('Not activated yet, closing or redirecting', status);
window.close();
window.location.href = '/';
return;
}
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = Client.getConfig().version;
} else if (localStorage.version !== Client.getConfig().version) {
localStorage.version = Client.getConfig().version;
window.location.reload(true);
}
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
Client.getInstalledApps().forEach(function (app) {
$scope.apps.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons
});
});
// activate pre-selected log from query otherwise choose the first one
$scope.selected = $scope.apps.find(function (e) { return e.value === search.id; });
if (!$scope.selected) $scope.selected = $scope.apps[0];
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showTerminal();
});
});
});
// setup all the dialog focus handling
['downloadFileModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
}]);

69
webadmin/src/js/update.js Normal file
View File

@@ -0,0 +1,69 @@
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', '$interval', function ($scope, $http, $interval) {
$scope.title = '';
$scope.percent = 0;
$scope.message = '';
$scope.error = false;
$scope.loadWebadmin = function () {
window.location.href = '/';
};
function fetchProgress() {
$http.get('/api/v1/cloudron/progress').success(function(data, status) {
if (status === 404) return; // just wait until we create the progress.json on the server side
if (status !== 200 || typeof data !== 'object') return console.error('Invalid response for progress', status, data);
if (!data.update && !data.migrate) return $scope.loadWebadmin();
if (data.update) {
if (data.update.percent >= 100) {
return $scope.loadWebadmin();
} else if (data.update.percent === -1) {
$scope.title = 'Update Error';
$scope.error = true;
$scope.message = data.update.message;
} else {
if (data.backup && data.backup.percent < 100) {
$scope.title = 'Backup in progress...';
$scope.percent = data.backup.percent < 0 ? 5 : (data.backup.percent / 100) * 50; // never show 0 as it looks like nothing happens
$scope.message = data.backup.message;
} else {
$scope.title = 'Update in progress...';
$scope.percent = 50 + ((data.update.percent / 100) * 50); // first half is backup
$scope.message = data.update.message;
}
}
} else { // migrating
if (data.migrate.percent === -1) {
$scope.title = 'Migration Error';
$scope.error = true;
$scope.message = data.migrate.message;
} else {
$scope.title = 'Migration in progress...';
$scope.percent = data.migrate.percent;
$scope.message = data.migrate.message;
if (!data.migrate.info) return;
// check if the new domain is available via the appstore (cannot use cloudron
// directly as we might hit NXDOMAIN)
$http.get(data.apiServerOrigin + '/api/v1/boxes/' + data.migrate.info.domain + '/status').success(function(data2, status) {
if (status === 200 && data2.status === 'ready') {
window.location = 'https://my.' + data.migrate.info.domain;
}
});
}
}
}).error(function (data, status) {
console.error('Error getting progress', status, data);
});
}
$interval(fetchProgress, 2000);
fetchProgress();
}]);