Merge remote-tracking branch 'origin/master' into feature/gcs
# Conflicts: # webadmin/src/views/certs.js # webadmin/src/views/settings.js
This commit is contained in:
@@ -215,7 +215,7 @@
|
||||
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
|
||||
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
|
||||
<li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li>
|
||||
<li ng-show="user.admin"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> Domain & Certs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains</a></li>
|
||||
<li ng-show="user.admin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
|
||||
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
|
||||
@@ -306,6 +306,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
var data = {
|
||||
appStoreId: id + '@' + manifest.version,
|
||||
location: config.location,
|
||||
domain: config.domain,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
cert: config.cert,
|
||||
@@ -349,6 +350,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
var data = {
|
||||
appId: id,
|
||||
location: config.location,
|
||||
domain: config.domain,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
cert: config.cert,
|
||||
@@ -466,20 +468,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setDnsConfig = function (dnsConfig, callback) {
|
||||
post('/api/v1/settings/dns_config', dnsConfig).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getDnsConfig = function (callback) {
|
||||
get('/api/v1/settings/dns_config').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setAutoupdatePattern = function (pattern, callback) {
|
||||
post('/api/v1/settings/autoupdate_pattern', { pattern: pattern }).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
@@ -587,6 +575,19 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.restore = function (backupConfig, backupId, version, callback) {
|
||||
var data = {
|
||||
backupConfig: backupConfig,
|
||||
backupId: backupId,
|
||||
version: version
|
||||
};
|
||||
|
||||
post('/api/v1/cloudron/restore', data).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getEventLogs = function (action, search, page, perPage, callback) {
|
||||
var config = {
|
||||
params: {
|
||||
@@ -1160,6 +1161,64 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
// Domains
|
||||
Client.prototype.getDomains = function (callback) {
|
||||
get('/api/v1/domains').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data.domains);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getDomain = function (domain, callback) {
|
||||
get('/api/v1/domains/' + domain).success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.addDomain = function (domain, config, fallbackCertificate, callback) {
|
||||
var data = {
|
||||
domain: domain,
|
||||
config: config
|
||||
};
|
||||
|
||||
if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate;
|
||||
|
||||
post('/api/v1/domains', data).success(function (data, status) {
|
||||
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.updateDomain = function (domain, config, fallbackCertificate, callback) {
|
||||
var data = {
|
||||
config: config
|
||||
};
|
||||
|
||||
if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate;
|
||||
|
||||
put('/api/v1/domains/' + domain, data).success(function (data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.removeDomain = function (domain, password, callback) {
|
||||
var config = {
|
||||
data: {
|
||||
password: password
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
del('/api/v1/domains/' + domain, config).success(function (data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
client = new Client();
|
||||
return client;
|
||||
}]);
|
||||
|
||||
@@ -55,9 +55,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/debug', {
|
||||
controller: 'DebugController',
|
||||
templateUrl: 'views/debug.html'
|
||||
}).when('/certs', {
|
||||
controller: 'CertsController',
|
||||
templateUrl: 'views/certs.html'
|
||||
}).when('/domains', {
|
||||
controller: 'DomainsController',
|
||||
templateUrl: 'views/domains.html'
|
||||
}).when('/email', {
|
||||
controller: 'EmailController',
|
||||
templateUrl: 'views/email.html'
|
||||
|
||||
@@ -114,47 +114,41 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
};
|
||||
|
||||
function runConfigurationChecks() {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
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);
|
||||
|
||||
var actionScope;
|
||||
if (backupConfig.provider === 'noop') {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/settings';
|
||||
|
||||
// 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.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope);
|
||||
}
|
||||
|
||||
if (result.provider === 'caas') return;
|
||||
|
||||
Client.getBackupConfig(function (error, backupConfig) {
|
||||
Client.getMailRelay(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (backupConfig.provider === 'noop') {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/settings';
|
||||
// the email status checks are currently only useful when using Cloudron itself for relaying
|
||||
if (result.provider !== 'cloudron-smtp') return;
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
|
||||
// the email status checks are currently only useful when using Cloudron itself for relaying
|
||||
if (result.provider !== 'cloudron-smtp') return;
|
||||
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/email';
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
184
webadmin/src/js/restore.js
Normal file
184
webadmin/src/js/restore.js
Normal file
@@ -0,0 +1,184 @@
|
||||
'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.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: '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 === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.backupFolder;
|
||||
}
|
||||
|
||||
var version = $scope.backupId.match(/_v(\d+.\d+.\d+)/);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}]);
|
||||
@@ -38,7 +38,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
|
||||
}
|
||||
});
|
||||
|
||||
// keep in sync with certs.js
|
||||
// keep in sync with domains.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
|
||||
@@ -108,10 +108,10 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
|
||||
};
|
||||
|
||||
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
|
||||
throw "fields_missing";
|
||||
throw 'fields_missing';
|
||||
}
|
||||
} catch(e) {
|
||||
$scope.dnsCredentials.error = "Cannot parse Google Service Account Key";
|
||||
$scope.dnsCredentials.error = 'Cannot parse Google Service Account Key';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +134,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
|
||||
}
|
||||
|
||||
waitForDnsSetup();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function waitForDnsSetup() {
|
||||
|
||||
159
webadmin/src/restore.html
Normal file
159
webadmin/src/restore.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Restore </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script src="3rdparty/js/angular.min.js"></script>
|
||||
<script src="3rdparty/js/angular-loader.min.js"></script>
|
||||
<script src="3rdparty/js/angular-md5.min.js"></script>
|
||||
<script src="3rdparty/js/angular-ui-notification.min.js"></script>
|
||||
<script src="3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script src="3rdparty/js/tld.js"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script src="js/restore.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="RestoreController">
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>Downloading backup</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="initialized && !busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="restore()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<p class="has-error text-center" ng-show="error">{{ error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProvider" ng-change=clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupFolder }" ng-show="provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="busy" placeholder="URL of Minio/S3 Compatible" ng-required="provider === 'minio' || provider === 's3-v4-compat'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
|
||||
Accept Self-signed certificate
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.bucket }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.secretAccessKey }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">Storage Format</label>
|
||||
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }" ng-show="provider !== 'noop' && format === 'tgz'">
|
||||
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional)</label>
|
||||
<input type="text" class="form-control" ng-model="key" id="inputConfigureBackupKey" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupId }">
|
||||
<label class="control-label" for="inputConfigureBackupId">Backup ID</label>
|
||||
|
||||
<input type="text" class="form-control" ng-model="backupId" name="inputConfigureBackupId" placeholder="Backup Id" required ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -84,11 +84,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<input type="submit" class="btn btn-primary" ng-disabled="setupForm.$invalid" value="Done">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a href="/restore.html">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Cloudron Setup</h1>
|
||||
<h3>Provide a domain for your Cloudron</h3>
|
||||
<p>Apps will be installed on subdomains of this domain.</p>
|
||||
<h3>Provide the first domain for your Cloudron</h3>
|
||||
<p>Apps will be installed on subdomains.</p>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h3>Choose how the domain is managed</h3>
|
||||
<h3>Choose how this domain is managed</h3>
|
||||
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -140,6 +140,12 @@
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid || (isSubdomain && !isEnterprise)"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<small>You can setup a new Cloudron or restore from a backup in the next step</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,7 +213,7 @@ h1, h2, h3 {
|
||||
|
||||
.grid-item {
|
||||
padding: 10px;
|
||||
min-width: 205px;
|
||||
min-width: 225px;
|
||||
|
||||
.col-xs-12 {
|
||||
padding-left: 5px;
|
||||
@@ -714,7 +714,6 @@ multiselect.stretch {
|
||||
color: $brand-danger;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
||||
<form role="form" name="appConfigureForm" ng-submit="appConfigure.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
||||
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + config.fqdn) }}
|
||||
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + appConfigure.domain) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(false)">{{ config.fqdn }}</a>
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="useAltDomain(false, domain)">{{ domain }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" ng-click="useAltDomain(true)"><i class="fa fa-star"></i> External Domain</a>
|
||||
@@ -170,7 +170,7 @@
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
|
||||
<button type="button" class="btn btn-success" ng-click="appConfigure.submit()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,9 +230,14 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">{{ appInfo.app.manifest.title }}</h5>
|
||||
<h5 class="app-info-title">
|
||||
{{ appInfo.app.manifest.title }}
|
||||
<span class="app-info-meta text-small">Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a> </span>
|
||||
</h5>
|
||||
<br/>
|
||||
<span class="app-info-meta">Package version <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">{{ appInfo.app.manifest.version }}</a> </span>
|
||||
<span class="app-info-meta" ng-show="appInfo.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">Documentation</a> </span>
|
||||
<br/>
|
||||
<span class="app-info-meta">Last updated {{ appInfo.app.updateTime | prettyDate }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="app-postinstall-message" ng-hide="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
|
||||
@@ -260,7 +265,7 @@
|
||||
<p>{{ appError.app.message | prettyAppMessage }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="showConfigure(appError.app)">Repair</button>
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="appConfigure.show(appError.app)">Repair</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,7 +375,8 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
<div class="text-small text-muted" ng-show="domains.length > 1">{{ app.domain }}</div><br/>
|
||||
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -378,12 +384,12 @@
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.altDomain || app.location || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -395,7 +401,7 @@
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
<a href="" ng-click="showConfigure(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
|
||||
<a href="" ng-click="appConfigure.show(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
|
||||
<i ng-hide="(app | installError)" class="fa fa-pencil scale"></i>
|
||||
<i ng-show="(app | installError)" class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
@@ -418,11 +424,11 @@
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
<a href="" ng-click="appConfigure.show(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="app | installError">
|
||||
<a href="" ng-click="showConfigure(app)" title="Repair App"><i class="fa fa-wrench scale"></i></a>
|
||||
<a href="" ng-click="appConfigure.show(app)" title="Repair App"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -7,7 +7,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.dnsConfig = {};
|
||||
$scope.domains = [];
|
||||
$scope.groups = [];
|
||||
$scope.users = [];
|
||||
$scope.mailConfig = {};
|
||||
@@ -17,6 +17,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
domain: '',
|
||||
location: '',
|
||||
usingAltDomain: false,
|
||||
advancedVisible: false,
|
||||
@@ -51,6 +52,129 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
|
||||
isAltDomainNaked: function () {
|
||||
return ngTld.isNakedDomain($scope.appConfigure.location);
|
||||
},
|
||||
|
||||
show: function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.altDomain || app.location;
|
||||
$scope.appConfigure.domain = app.domain;
|
||||
$scope.appConfigure.usingAltDomain = !!app.altDomain;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope. Option = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// 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.appConfigure.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.appConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
}
|
||||
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.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.appConfigure.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.appConfigure.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.appConfigure.portBindings[env] = app.portBindings[env];
|
||||
$scope.appConfigure.portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.appConfigure.portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('#appConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appConfigure.busy = true;
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.xFrameOptions = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appConfigure.portBindings) {
|
||||
if ($scope.appConfigure.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
|
||||
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
|
||||
domain: $scope.appConfigure.usingAltDomain ? undefined : $scope.appConfigure.domain,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
|
||||
robotsTxt: $scope.appConfigure.robotsTxt,
|
||||
enableBackup: $scope.appConfigure.enableBackup
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appConfigure.error.location = 'This name is already taken.';
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
|
||||
$scope.appConfigure.error.xFrameOptions = error.message;
|
||||
$scope.appConfigureForm.xFrameOptions.$setPristine();
|
||||
$('#appConfigureXFrameOptionsInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
$('#appConfigureModal').modal('hide');
|
||||
|
||||
$scope.reset();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,6 +269,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
// reset configure dialog
|
||||
$scope.appConfigure.error = {};
|
||||
$scope.appConfigure.app = {};
|
||||
$scope.appConfigure.domain = '';
|
||||
$scope.appConfigure.location = '';
|
||||
$scope.appConfigure.advancedVisible = false;
|
||||
$scope.appConfigure.usingAltDomain = false;
|
||||
@@ -218,8 +343,9 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.useAltDomain = function (use) {
|
||||
$scope.useAltDomain = function (use, domain) {
|
||||
$scope.appConfigure.usingAltDomain = use;
|
||||
$scope.appConfigure.domain = domain;
|
||||
|
||||
if (use) {
|
||||
$scope.appConfigure.location = '';
|
||||
@@ -228,127 +354,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.altDomain || app.location;
|
||||
$scope.appConfigure.usingAltDomain = !!app.altDomain;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope. Option = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
|
||||
$scope.appConfigure.robotsTxt = app.robotsTxt;
|
||||
$scope.appConfigure.enableBackup = app.enableBackup;
|
||||
|
||||
// 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.appConfigure.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.appConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
}
|
||||
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
||||
$scope.appConfigure.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.appConfigure.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.appConfigure.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.appConfigure.portBindings[env] = app.portBindings[env];
|
||||
$scope.appConfigure.portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.appConfigure.portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('#appConfigureModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doConfigure = function () {
|
||||
$scope.appConfigure.busy = true;
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.xFrameOptions = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appConfigure.portBindings) {
|
||||
if ($scope.appConfigure.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
|
||||
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
|
||||
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
|
||||
robotsTxt: $scope.appConfigure.robotsTxt,
|
||||
enableBackup: $scope.appConfigure.enableBackup
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appConfigure.error.location = 'This name is already taken.';
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
|
||||
$scope.appConfigure.error.xFrameOptions = error.message;
|
||||
$scope.appConfigureForm.xFrameOptions.$setPristine();
|
||||
$('#appConfigureXFrameOptionsInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
$('#appConfigureModal').modal('hide');
|
||||
|
||||
$scope.reset();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showInformation = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
@@ -465,14 +470,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
}
|
||||
|
||||
function fetchDnsConfig() {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
function getDomains() {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchDnsConfig, 5000);
|
||||
return $timeout(getDomains, 5000);
|
||||
}
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
$scope.domains = result.map(function (d) { return d.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -499,7 +504,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
if ($scope.user.admin) {
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchDnsConfig();
|
||||
getDomains();
|
||||
getMailConfig();
|
||||
getBackupConfig();
|
||||
}
|
||||
|
||||
@@ -21,8 +21,16 @@
|
||||
<label class="control-label" for="appInstallLocationInput">Location {{ appInstall.error.location }} </label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.location" id="appInstallLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<div class="input-group-addon">
|
||||
{{ !appInstall.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{{ (config.isCustomDomain ? '.' : '-') + appInstall.domain }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.domain = domain">{{ domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,7 +286,6 @@
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fa fa-line-chart"></i> Project Management</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fa fa-wikipedia-w"></i> Wiki</a>
|
||||
<br/>
|
||||
<!-- <a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'testing' }" category="testing" ng-show="config.developerMode">Testing</a> -->
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
@@ -12,7 +12,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.dnsConfig = {};
|
||||
$scope.domains = [];
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
@@ -37,6 +37,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
error: {},
|
||||
app: {},
|
||||
location: '',
|
||||
domain: '',
|
||||
portBindings: {},
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
@@ -57,6 +58,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.domain = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
@@ -102,6 +104,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.domain = $scope.config.fqdn; // FIXME needs to come from domains dropdown
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
@@ -145,6 +148,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
domain: $scope.appInstall.domain,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
@@ -492,14 +496,14 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function fetchDnsConfig() {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
function getDomains() {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchDnsConfig, 5000);
|
||||
return $timeout(getDomains, 5000);
|
||||
}
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
$scope.domains = result.map(function (d) { return d.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -557,7 +561,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchDnsConfig();
|
||||
getDomains();
|
||||
getMailConfig();
|
||||
|
||||
fetchAppstoreConfig(function (error) {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
<div class="modal fade" id="dnsCredentialsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure DNS</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
|
||||
<label class="control-label" for="customDomainId">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" placeholder="example.com" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="dnsCredentialsProvider">DNS API provider</label>
|
||||
<select class="form-control" id="dnsCredentialsProvider" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label" for="dnsCredentialsDigitalOceanToken">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" id="dnsCredentialsDigitalOceanToken" name="digitalOceanToken" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" for="dnsCredentialsCloudflareToken">Cloudflare token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" id="dnsCredentialsCloudflareToken" name="cloudflareToken" placeholder="API Key" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" for="dnsCredentialsCloudflareEmail">Cloudflare email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" id="dnsCredentialsCloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- this will be autofilled by most browsers regardless of the attribute, since the next field is a password field.... -->
|
||||
<input type="text" class="form-control hide">
|
||||
|
||||
<!-- all provider -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="dnsCredentialsForm.$invalid || dnsCredentials.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
This domain must be hosted on <a href="https://console.cloud.google.com/net-services/dns/zones" target="_blank">Google Cloud DNS</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
This domain must be hosted on <a href="https://www.cloudflare.com" target="_blank">Cloudflare</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.customDomain || 'example.com' }}</b> and <b>{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'manual'">
|
||||
Setup an <i>A</i> record for <b>{{ config.adminLocation }}.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="setDnsCredentials()" ng-disabled="dnsCredentialsForm.$invalid || dnsCredentials.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i>
|
||||
<span ng-show="dnsCredentials.customDomain === config.fqdn">Save</span>
|
||||
<span ng-show="dnsCredentials.customDomain !== config.fqdn">Change Domain</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domain & Certificates</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Domain</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!config.isCustomDomain">To use a custom domain, configure your domain to use <a target="_blank" href="https://aws.amazon.com/route53/">Route53.</a> Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Domain name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.fqdn }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">DNS provider</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.provider }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && !dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider is configured. All DNS records need to be setup manually.
|
||||
To avoid manual setup for each installed app, set a DNS API provider.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'manual' && dnsConfig.wildcard">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="dnsConfig.provider === 'noop'">
|
||||
<td colspan="2">
|
||||
<br/>
|
||||
No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Access key id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId || 'unset' }}</td>
|
||||
</tr>
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'route53'">
|
||||
<td class="text-muted" style="vertical-align: top;">Secret access key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.secretAccessKey"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain && dnsConfig.provider === 'digitalocean'">
|
||||
<td class="text-muted" style="vertical-align: top;">DigitalOcean token</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;" ng-click-reveal="dnsConfig.token"><i>hidden</i></td>
|
||||
</tr>
|
||||
|
||||
<!-- add some space -->
|
||||
<tr>
|
||||
<td><br/></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><button class="btn btn-outline btn-primary" ng-click="showChangeDnsCredentials()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>SSL Certificates</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="!config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
Certificates can only by set for custom domains.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<p>Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.</p>
|
||||
<br/>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="defaultCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row hide">
|
||||
<div class="col-md-12">
|
||||
<form name="adminCertForm" ng-submit="setAdminCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="adminCertInput">Settings Certificate</label>
|
||||
<p>This certificate will be used for this Settings application.</p>
|
||||
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="adminCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,258 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = null;
|
||||
|
||||
// keep in sync with setupdns.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.defaultCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.adminCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.dnsCredentials = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
customDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
provider: 'route53',
|
||||
password: ''
|
||||
};
|
||||
|
||||
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('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
|
||||
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDefaultCert = function () {
|
||||
$scope.defaultCert.busy = true;
|
||||
$scope.defaultCert.error = null;
|
||||
$scope.defaultCert.success = false;
|
||||
|
||||
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.defaultCert.error = error.message;
|
||||
} else {
|
||||
$scope.defaultCert.success = true;
|
||||
$scope.defaultCert.certificateFileName = '';
|
||||
$scope.defaultCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.defaultCert.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setAdminCert = function () {
|
||||
$scope.adminCert.busy = true;
|
||||
$scope.adminCert.error = null;
|
||||
$scope.adminCert.success = false;
|
||||
|
||||
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.adminCert.error = error.message;
|
||||
} else {
|
||||
$scope.adminCert.success = true;
|
||||
$scope.adminCert.certificateFileName = '';
|
||||
$scope.adminCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.adminCert.busy = false;
|
||||
|
||||
// attempt to reload to make the browser get the new certs
|
||||
window.location.reload(true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.dnsCredentials.success = false;
|
||||
|
||||
var migrateDomain = $scope.dnsCredentials.customDomain !== $scope.config.fqdn;
|
||||
|
||||
var data = {
|
||||
provider: $scope.dnsCredentials.provider
|
||||
};
|
||||
|
||||
// special case the wildcard provider
|
||||
if (data.provider === 'wildcard') {
|
||||
data.provider = 'manual';
|
||||
data.wildcard = true;
|
||||
}
|
||||
|
||||
if (data.provider === 'route53') {
|
||||
data.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
data.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (data.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: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (data.provider === 'digitalocean') {
|
||||
data.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (data.provider === 'cloudflare') {
|
||||
data.token = $scope.dnsCredentials.cloudflareToken;
|
||||
data.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
}
|
||||
|
||||
var func;
|
||||
if (migrateDomain) {
|
||||
data.domain = $scope.dnsCredentials.customDomain;
|
||||
func = Client.migrate.bind(Client, data, $scope.dnsCredentials.password);
|
||||
} else {
|
||||
func = Client.setDnsConfig.bind(Client, data);
|
||||
}
|
||||
|
||||
func(function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.error = error.message;
|
||||
} else {
|
||||
$scope.dnsCredentials.success = true;
|
||||
|
||||
$('#dnsCredentialsModal').modal('hide');
|
||||
|
||||
dnsCredentialsReset();
|
||||
|
||||
if (migrateDomain) window.location.href = '/update.html';
|
||||
}
|
||||
|
||||
$scope.dnsCredentials.busy = false;
|
||||
|
||||
// reload the dns config
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function dnsCredentialsReset() {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.dnsCredentials.success = false;
|
||||
$scope.dnsCredentials.error = null;
|
||||
|
||||
$scope.dnsCredentials.provider = '';
|
||||
$scope.dnsCredentials.customDomain = '';
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = '';
|
||||
$scope.dnsCredentials.gcdnsKey.content = '';
|
||||
$scope.dnsCredentials.digitalOceanToken = '';
|
||||
$scope.dnsCredentials.cloudflareToken = '';
|
||||
$scope.dnsCredentials.cloudflareEmail = '';
|
||||
$scope.dnsCredentials.password = '';
|
||||
|
||||
$scope.dnsCredentialsForm.$setPristine();
|
||||
$scope.dnsCredentialsForm.$setUntouched();
|
||||
|
||||
$('#customDomainId').focus();
|
||||
}
|
||||
|
||||
$scope.showChangeDnsCredentials = function () {
|
||||
dnsCredentialsReset();
|
||||
|
||||
// clear the input box for non-custom domain
|
||||
$scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : '';
|
||||
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
|
||||
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
|
||||
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = '';
|
||||
$scope.dnsCredentials.gcdnsKey.content = '';
|
||||
if ($scope.dnsConfig.provider === 'gcdns') {
|
||||
$scope.dnsCredentials.gcdnsKey.keyFileName = $scope.dnsConfig.credentials && $scope.dnsConfig.credentials.client_email;
|
||||
$scope.dnsCredentials.gcdnsKey.content = JSON.stringify({
|
||||
"project_id": $scope.dnsConfig.projectId,
|
||||
"credentials": $scope.dnsConfig.credentials
|
||||
});
|
||||
}
|
||||
$scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : '';
|
||||
$scope.dnsCredentials.cloudflareToken = $scope.dnsConfig.provider === 'cloudflare' ? $scope.dnsConfig.token : '';
|
||||
$scope.dnsCredentials.cloudflareEmail = $scope.dnsConfig.email;
|
||||
|
||||
$scope.dnsCredentials.provider = $scope.dnsConfig.provider === 'caas' ? 'route53' : $scope.dnsConfig.provider;
|
||||
$scope.dnsCredentials.provider = ($scope.dnsCredentials.provider === 'manual' && $scope.dnsConfig.wildcard) ? 'wildcard' : $scope.dnsCredentials.provider;
|
||||
|
||||
$('#dnsCredentialsModal').modal('show');
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['dnsCredentialsModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -68,8 +68,8 @@
|
||||
<div class="logs-controls">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<uib-tabset active="active">
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
<uib-tab index="0" heading="Logs" select="showLogs()"></uib-tab>
|
||||
<uib-tab index="1" heading="Terminal" select="showTerminal()"></uib-tab>
|
||||
</uib-tabset>
|
||||
<select class="form-control pull-right inline" ng-options="log.name for log in logs track by log.value" ng-model="selected"></select>
|
||||
|
||||
|
||||
197
webadmin/src/views/domains.html
Normal file
197
webadmin/src/views/domains.html
Normal file
@@ -0,0 +1,197 @@
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">Add Domain</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">Configure {{ domainConfigure.domain.domain }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': domainConfigureForm.newDomain.$invalid }" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS API provider</label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">Cloudflare token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" placeholder="API Key" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">Cloudflare email</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'gcdns'">
|
||||
This domain must be hosted on <a href="https://console.cloud.google.com/net-services/dns/zones" target="_blank">Google Cloud DNS</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
This domain must be hosted on <a href="https://www.cloudflare.com" target="_blank">Cloudflare</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> and <b>{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<p ng-show="domainConfigure.provider === 'manual'">
|
||||
All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- Wildcard certificates -->
|
||||
<label class="control-label">Fallback Certificate (optional)</label>
|
||||
<p>
|
||||
Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.
|
||||
If provided, this wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.
|
||||
</p>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="domainConfigure.busy"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain remove -->
|
||||
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really remove {{ domainRemove.domain.domain }} ?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form role="form" name="domainRemoveForm" ng-submit="domainRemove.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid) || (!domainRemoveForm.password.$dirty && domainRemove.error) }">
|
||||
<label class="control-label">Provide your password to confirm this action</label>
|
||||
<div class="control-label" ng-show="(domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid) || (!domainRemoveForm.password.$dirty && domainRemove.error)">
|
||||
<small ng-show=" domainRemoveForm.password.$dirty && domainRemoveForm.password.$invalid">Password required</small>
|
||||
<small ng-show="!domainRemoveForm.password.$dirty && domainRemove.error">{{ domainRemove.error }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="domainRemove.password" id="domainRemovePasswordInput" name="password" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainRemoveForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemoveForm.$invalid || domainRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="domainRemove.busy"></i> Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domains <button class="btn btn-primary btn-outline pull-right" ng-hide="true" ng-click="domainConfigure.show()"><i class="fa fa-plus"></i> Add Domain</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-o-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="text-left hidden-xs hidden-sm">Provider</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td class="elide-table-cell">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm">
|
||||
{{ domain.config.provider }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" ng-show="domain.config.provider !== 'caas'" title="Edit Domain"><i class="fa fa-pencil"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-hide="true" ng-click="domainRemove.show(domain)" ng-show="domain.config.provider !== 'caas'" title="Remove Domain"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
261
webadmin/src/views/domains.js
Normal file
261
webadmin/src/views/domains.js
Normal file
@@ -0,0 +1,261 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = null;
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
|
||||
// keep in sync with setupdns.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' }
|
||||
];
|
||||
|
||||
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]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// We reused configure also for adding domains to avoid much code duplication
|
||||
$scope.domainConfigure = {
|
||||
adding: false,
|
||||
error: null,
|
||||
busy: false,
|
||||
domain: null,
|
||||
|
||||
// form model
|
||||
newDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
provider: 'route53',
|
||||
|
||||
fallbackCert: {
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
if (domain) {
|
||||
$scope.domainConfigure.domain = domain;
|
||||
$scope.domainConfigure.accessKeyId = domain.config.accessKeyId;
|
||||
$scope.domainConfigure.secretAccessKey = domain.config.secretAccessKey;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
if ($scope.domainConfigure.provider === 'gcdns') {
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email;
|
||||
$scope.domainConfigure.gcdnsKey.content = JSON.stringify({
|
||||
"project_id": domain.config.projectId,
|
||||
"credentials": domain.config.credentials
|
||||
});
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.config.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.config.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.config.email;
|
||||
|
||||
$scope.domainConfigure.provider = domain.config.provider;
|
||||
$scope.domainConfigure.provider = ($scope.domainConfigure.provider === 'manual' && domain.config.wildcard) ? 'wildcard' : domain.config.provider;
|
||||
} else {
|
||||
$scope.domainConfigure.adding = true;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainConfigure.busy = true;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
var data = {
|
||||
provider: $scope.domainConfigure.provider
|
||||
};
|
||||
|
||||
// special case the wildcard provider
|
||||
if (data.provider === 'wildcard') {
|
||||
data.provider = 'manual';
|
||||
data.wildcard = true;
|
||||
}
|
||||
|
||||
if (data.provider === 'route53') {
|
||||
data.accessKeyId = $scope.domainConfigure.accessKeyId;
|
||||
data.secretAccessKey = $scope.domainConfigure.secretAccessKey;
|
||||
} else if (data.provider === 'gcdns'){
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.domainConfigure.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.domainConfigure.error = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.domainConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (data.provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (data.provider === 'cloudflare') {
|
||||
data.token = $scope.domainConfigure.cloudflareToken;
|
||||
data.email = $scope.domainConfigure.cloudflareEmail;
|
||||
}
|
||||
|
||||
var fallbackCertificate = null;
|
||||
if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) {
|
||||
fallbackCertificate = {
|
||||
cert: $scope.domainConfigure.fallbackCert.certificateFile,
|
||||
key: $scope.domainConfigure.fallbackCert.keyFile
|
||||
};
|
||||
}
|
||||
|
||||
// choose the right api, since we reuse this for adding and configuring domains
|
||||
var func;
|
||||
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, data, fallbackCertificate);
|
||||
else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, data, fallbackCertificate);
|
||||
|
||||
func(function (error) {
|
||||
$scope.domainConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.domainConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('hide');
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
// reload the domains
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainConfigure.adding = false;
|
||||
$scope.domainConfigure.newDomain = '';
|
||||
|
||||
$scope.domainConfigure.busy = false;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
$scope.domainConfigure.provider = '';
|
||||
$scope.domainConfigure.accessKeyId = '';
|
||||
$scope.domainConfigure.secretAccessKey = '';
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
$scope.domainConfigure.digitalOceanToken = '';
|
||||
$scope.domainConfigure.cloudflareToken = '';
|
||||
$scope.domainConfigure.cloudflareEmail = '';
|
||||
|
||||
$scope.domainConfigureForm.$setPristine();
|
||||
$scope.domainConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
domain: null,
|
||||
password: '',
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
$scope.domainRemove.domain = domain;
|
||||
|
||||
$('#domainRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainRemove.busy = true;
|
||||
$scope.domainRemove.error = null;
|
||||
|
||||
Client.removeDomain($scope.domainRemove.domain.domain, $scope.domainRemove.password, function (error) {
|
||||
if (error && (error.statusCode === 403 || error.statusCode === 409)) {
|
||||
$scope.domainRemove.password = '';
|
||||
$scope.domainRemove.error = error.message;
|
||||
$scope.domainRemoveForm.password.$setPristine();
|
||||
$('#domainRemovePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#domainRemoveModal').modal('hide');
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
// reload the domains
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.domainRemove.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainRemove.busy = false;
|
||||
$scope.domainRemove.error = null;
|
||||
$scope.domainRemove.domain = null;
|
||||
$scope.domainRemove.password = '';
|
||||
|
||||
$scope.domainRemoveForm.$setPristine();
|
||||
$scope.domainRemoveForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
|
||||
document.getElementById('fallbackCertFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('fallbackKeyFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'keyFile', 'keyFileName');
|
||||
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -27,18 +27,32 @@
|
||||
</div>
|
||||
|
||||
<!-- Test email sent -->
|
||||
<div class="modal fade" id="testEmailSent" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Test Email Sent</h4>
|
||||
<h4 class="modal-title">Send test email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A test email was sent to {{ user.email }}. Please check the inbox of this account and verify the email was delivered.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Ok</button>
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">Email to</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="Email address" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy">
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,12 +195,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
|
||||
<h3>DNS Records</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas' && currentRelay.provider === 'cloudron-smtp'">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12">
|
||||
Set the following DNS records to guarantee email delivery:
|
||||
|
||||
@@ -196,7 +211,7 @@
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
@@ -211,8 +226,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
@@ -226,13 +243,13 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
<i ng-hide="email.refreshBusy" ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
|
||||
Outbound SMTP
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_port" class="panel-collapse collapse">
|
||||
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ relay.value }} </b> </p>
|
||||
</div>
|
||||
@@ -240,10 +257,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="rbl">
|
||||
<div class="row" ng-show="currentRelay.provider === 'cloudron-smtp'">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-hide="email.refreshBusy" ng-class="rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">
|
||||
IP Address Blacklist Check
|
||||
</a>
|
||||
@@ -262,7 +279,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button class="btn btn-primary pull-left" ng-click="sendTestEmail()">Send Test Email</button>
|
||||
<button class="btn btn-primary pull-left" ng-click="testEmail.show()">Send Test Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,16 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = {};
|
||||
$scope.currentRelay = {};
|
||||
$scope.relay = {};
|
||||
$scope.rbl = null;
|
||||
$scope.expectedDnsRecords = {};
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
dkim: { },
|
||||
spf: { },
|
||||
dmarc: { },
|
||||
ptr: { }
|
||||
};
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
@@ -81,6 +88,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
refresh: function () {
|
||||
$scope.email.refreshBusy = true;
|
||||
|
||||
collapseDnsRecords();
|
||||
|
||||
showExpectedDnsRecords(function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
@@ -150,20 +159,56 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
|
||||
Client.setMailRelay(data, function (error) {
|
||||
if (error) $scope.mailRelay.error = error.message;
|
||||
else $scope.mailRelay.success = true;
|
||||
|
||||
$scope.mailRelay.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.mailRelay.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentRelay = data;
|
||||
$scope.mailRelay.success = true;
|
||||
$scope.email.refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sendTestEmail = function () {
|
||||
Client.sentTestMail($scope.user.email, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.testEmail = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
$('#testEmailSent').modal('show');
|
||||
});
|
||||
mailTo: '',
|
||||
|
||||
clearForm: function () {
|
||||
$scope.testEmail.mailTo = '';
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
$scope.testEmail.mailTo = $scope.user.email;
|
||||
|
||||
$('#testEmailModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.testEmail.error = {};
|
||||
$scope.testEmail.busy = true;
|
||||
|
||||
Client.sentTestMail($scope.testEmail.mailTo, function (error) {
|
||||
$scope.testEmail.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.testEmail.error.generic = error.message;
|
||||
console.error(error);
|
||||
$('#inputTestMailTo').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#testEmailModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getMailConfig() {
|
||||
@@ -185,6 +230,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailRelay.relay.password = '';
|
||||
$scope.mailRelay.relay.serverApiToken = '';
|
||||
|
||||
$scope.currentRelay = relay;
|
||||
|
||||
if (relay.provider === 'postmark-smtp') {
|
||||
$scope.mailRelay.relay.serverApiToken = relay.username;
|
||||
} else if (relay.provider === 'sendgrid-smtp') {
|
||||
@@ -203,33 +250,50 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
// TODO this currently assumes the config.fqdn is the mail domain
|
||||
function getDnsConfig() {
|
||||
Client.getDnsConfig(function (error, dnsConfig) {
|
||||
Client.getDomain($scope.config.fqdn, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = dnsConfig;
|
||||
$scope.dnsConfig = result.config;
|
||||
});
|
||||
}
|
||||
|
||||
function collapseDnsRecords() {
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$('#collapse_dns_' + type).collapse('hide');
|
||||
});
|
||||
|
||||
$('#collapse_outbound_smtp').collapse('hide');
|
||||
$('#collapse_rbl').collapse('hide');
|
||||
}
|
||||
|
||||
function showExpectedDnsRecords(callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.expectedDnsRecords = result.dns;
|
||||
$scope.relay = result.relay;
|
||||
$scope.rbl = result.rbl;
|
||||
|
||||
// open the record details if they are not correct
|
||||
for (var type in $scope.expectedDnsRecords) {
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$scope.expectedDnsRecords[type] = result.dns[type] || {};
|
||||
|
||||
if (!$scope.expectedDnsRecords[type].status) {
|
||||
$('#collapse_dns_' + type).collapse('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!$scope.relay.status) {
|
||||
$('#collapse_dns_port').collapse('show');
|
||||
$('#collapse_outbound_smtp').collapse('show');
|
||||
}
|
||||
|
||||
if (!$scope.rbl.status) {
|
||||
$('#collapse_rbl').collapse('show');
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -285,5 +349,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.email.refresh();
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['testEmailModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<fieldset>
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputDeveloperModeChangePassword">Give your password to verify that you are performing that action</label>
|
||||
<label class="control-label">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!planChangeForm.password.$dirty && planChange.error.password) || (planChangeForm.password.$dirty && planChangeForm.password.$invalid)">
|
||||
<small ng-show=" planChangeForm.password.$dirty && planChangeForm.password.$invalid">A password is required</small>
|
||||
<small ng-show="!planChangeForm.password.$dirty && planChange.error.password">Wrong password</small>
|
||||
@@ -125,7 +125,7 @@
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/references/selfhosting/#backups" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearForm()></select>
|
||||
</div>
|
||||
|
||||
@@ -183,6 +183,11 @@
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
@@ -403,7 +408,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
@@ -412,7 +417,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-md-11" ng-show="createBackup.busy">
|
||||
<p class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ createBackup.detail || 'Syncing ...' }}
|
||||
@@ -420,14 +425,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.provider !== 'caas'">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
|
||||
<p ng-hide="createBackup.busy">
|
||||
<div class="has-error" ng-show="createBackup.percent === 100 && createBackup.result">{{ createBackup.result }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<div class="col-md-6 text-right" ng-show="backupConfig.provider !== 'caas'">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.doCreateBackup()" ng-disabled="createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
|
||||
@@ -40,10 +40,15 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
{ 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: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'DigitalOcean Spaces NYC3', value: 'digitalocean-spaces' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Minio', value: 'minio' },
|
||||
@@ -422,7 +427,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v2';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.endpoint = 'https://nyc3.digitaloceanspaces.com';
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs'){
|
||||
|
||||
@@ -31,7 +31,7 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
|
||||
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error;
|
||||
$scope.feedback.error = error.message;
|
||||
} else {
|
||||
$scope.feedback.success = true;
|
||||
resetFeedback();
|
||||
|
||||
Reference in New Issue
Block a user